Compare commits

..
Author SHA1 Message Date
mickael emirkanian 1c3c00dd7f ci: update workflows to test action
test the tracebit action

Signed-off-by: mickael emirkanian <mickael.emirkanian@docker.com>
2026-09-24 14:19:55 -04:00
435 changed files with 12021 additions and 20296 deletions
-59
View File
@@ -1,59 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
// https://docs.renovatebot.com/modules/manager/github-actions/
// https://docs.renovatebot.com/presets-helpers/
"helpers:pinGitHubActionDigestsToSemver",
":gitSignOff"
],
"gomod": {
"managerFilePatterns": ["/(^|/)vendor\\.mod$/"]
},
"postUpdateOptions": ["gomodTidy"],
"minimumReleaseAge": "3 days",
"internalChecksFilter": "strict",
"packageRules": [
{
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"semanticCommits": "disabled",
"commitMessagePrefix": "vendor:"
},
{
"matchManagers": ["gomod"],
"matchFileNames": ["cmd/docker-trust/go.mod"],
// The trust module does not vendor dependencies; vendor/ belongs to the root module.
"postUpdateOptions": ["gomodSkipVendor"],
"semanticCommits": "disabled",
"commitMessagePrefix": "cmd/docker-trust:"
},
{
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"groupName": "golang.org/x packages",
"groupSlug": "golang-x",
"matchDepNames": ["/^golang\\.org\\/x\\//"]
},
{
"matchManagers": ["github-actions"],
"minimumReleaseAge": "7 days",
"addLabels": ["area/ci", "status/2-code-review"]
},
{
"matchManagers": ["github-actions"],
"groupName": "docker GitHub Actions",
"groupSlug": "docker-actions",
"matchDepNames": ["/^docker\\//"]
},
{
// Keep all the google.golang.org/genproto modules at the same version.
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"groupName": "google.golang.org/genproto packages",
"groupSlug": "google-genproto",
"matchDepNames": ["/^google\\.golang\\.org\\/genproto(\\/|$)/"],
"schedule": ["every month"]
}
]
}
+38 -6
View File
@@ -33,6 +33,14 @@ jobs:
outputs: outputs:
matrix: ${{ steps.platforms.outputs.matrix }} matrix: ${{ steps.platforms.outputs.matrix }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -63,12 +71,20 @@ jobs:
- "" - ""
- glibc - glibc
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- -
name: Build name: Build
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0 uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
set: | set: |
@@ -91,7 +107,7 @@ jobs:
PLATFORM: ${{ matrix.platform }} PLATFORM: ${{ matrix.platform }}
- -
name: Upload artifacts name: Upload artifacts
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: ${{ env.ARTIFACT_NAME }} name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/* path: /tmp/out/*
@@ -99,7 +115,7 @@ jobs:
bin-image: bin-image:
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }} if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
uses: docker/github-builder/.github/workflows/bake.yml@a31d2311da4d55e21b978c1c52bab82ba97d51ee # v1.19.0 uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0
permissions: permissions:
contents: read # same as global permission contents: read # same as global permission
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
@@ -130,6 +146,14 @@ jobs:
outputs: outputs:
matrix: ${{ steps.platforms.outputs.matrix }} matrix: ${{ steps.platforms.outputs.matrix }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -154,12 +178,20 @@ jobs:
matrix: matrix:
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }} platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- -
name: Build name: Build
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0 uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
with: with:
targets: plugins-cross targets: plugins-cross
set: | set: |
+4 -4
View File
@@ -64,18 +64,18 @@ jobs:
name: Update Go name: Update Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: "1.27.4" go-version: "1.26.8"
cache: false cache: false
- -
name: Initialize CodeQL name: Initialize CodeQL
uses: github/codeql-action/init@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with: with:
languages: go languages: go
- -
name: Autobuild name: Autobuild
uses: github/codeql-action/autobuild@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
- -
name: Perform CodeQL Analysis name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with: with:
category: "/language:go" category: "/language:go"
+10 -6
View File
@@ -26,9 +26,6 @@ on:
jobs: jobs:
tests: tests:
permissions:
contents: read
id-token: write # needed for CodeCov; https://github.com/codecov/codecov-action/blob/v7.1.1/README.md#using-oidc
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
strategy: strategy:
fail-fast: false fail-fast: false
@@ -40,12 +37,19 @@ jobs:
- alpine - alpine
- debian - debian
engine-version: engine-version:
- master # moby/moby-bin:master
- rc # latest rc - rc # latest rc
- 29 # latest - 29 # latest
- 28 # latest - 1 - 28 # latest - 1
- 25 # mirantis lts - 25 # mirantis lts
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -69,7 +73,7 @@ jobs:
docker info docker info
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- -
name: Run ${{ matrix.target }} name: Run ${{ matrix.target }}
run: | run: |
@@ -83,4 +87,4 @@ jobs:
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
files: ./build/coverage/coverage.txt files: ./build/coverage/coverage.txt
use_oidc: true token: ${{ secrets.CODECOV_TOKEN }}
+9 -1
View File
@@ -20,6 +20,14 @@ jobs:
if: github.event.pull_request.head.repo.fork if: github.event.pull_request.head.repo.fork
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Save event context - name: Save event context
env: env:
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -35,7 +43,7 @@ jobs:
fi fi
- name: Upload context - name: Upload context
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: pr-review-context name: pr-review-context
path: context/ path: context/
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs: jobs:
review: review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@aaa657c53cc69ed15683cd8a985d86663a0e1594 # v2.1.0 uses: docker/docker-agent-action/.github/workflows/review-pr.yml@3654ec391b070e247de2e2e3c1ff2519b6378b3a # v2.0.9
permissions: permissions:
contents: read # Read repository files and PR diffs contents: read # Read repository files and PR diffs
pull-requests: write # Post review comments pull-requests: write # Post review comments
+23 -13
View File
@@ -26,17 +26,22 @@ on:
jobs: jobs:
ctn: ctn:
permissions:
contents: read
id-token: write # needed for CodeCov; https://github.com/codecov/codecov-action/blob/v7.1.1/README.md#using-oidc
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- -
name: Test name: Test
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0 uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
with: with:
targets: test-coverage targets: test-coverage
- -
@@ -44,12 +49,9 @@ jobs:
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
files: ./build/coverage/coverage.txt files: ./build/coverage/coverage.txt
use_oidc: true token: ${{ secrets.CODECOV_TOKEN }}
host: host:
permissions:
contents: read
id-token: write # needed for CodeCov; https://github.com/codecov/codecov-action/blob/v7.1.1/README.md#using-oidc
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
GOPATH: ${{ github.workspace }} GOPATH: ${{ github.workspace }}
@@ -59,11 +61,19 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: 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-15 # macOS 15 on arm64 (Apple Silicon M1)
- macos-26 # macOS 26 on arm64 (Apple Silicon M1)
- macos-26-intel # macOS 26 on Intel
# - 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 # - 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: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -74,7 +84,7 @@ jobs:
name: Set up Go name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: "1.27.4" go-version: "1.26.8"
cache: false cache: false
- -
name: Test name: Test
@@ -92,4 +102,4 @@ jobs:
with: with:
files: /tmp/coverage.txt files: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
use_oidc: true token: ${{ secrets.CODECOV_TOKEN }}
+8
View File
@@ -13,6 +13,14 @@ jobs:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Validate milestone matches VERSION - name: Validate milestone matches VERSION
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env: env:
+24
View File
@@ -18,6 +18,14 @@ jobs:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
timeout-minutes: 120 # guardrails timeout for the whole job timeout-minutes: 120 # guardrails timeout for the whole job
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Missing `area/` label - 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: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
run: | run: |
@@ -39,6 +47,14 @@ jobs:
PR_BODY: | PR_BODY: |
${{ github.event.pull_request.body }} ${{ github.event.pull_request.body }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Check changelog description - name: Check changelog description
run: | run: |
# Extract the `markdown changelog` note code block # Extract the `markdown changelog` note code block
@@ -75,6 +91,14 @@ jobs:
env: env:
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
# Backports or PR that target a release branch directly should mention the target branch in the title, for example: # Backports or PR that target a release branch directly should mention the target branch in the title, for example:
# [X.Y backport] Some change that needs backporting to X.Y # [X.Y backport] Some change that needs backporting to X.Y
# [X.Y] Change directly targeting the X.Y branch # [X.Y] Change directly targeting the X.Y branch
+34 -2
View File
@@ -36,9 +36,17 @@ jobs:
- validate-vendor - validate-vendor
- update-authors # ensure authors update target runs fine - update-authors # ensure authors update target runs fine
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Run name: Run
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0 uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
@@ -46,6 +54,14 @@ jobs:
validate-md: validate-md:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -74,6 +90,14 @@ jobs:
- yamldocs # ensure yamldocs target runs fine - yamldocs # ensure yamldocs target runs fine
- manpages # ensure manpages target runs fine - manpages # ensure manpages target runs fine
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -91,6 +115,14 @@ jobs:
GOPATH: ${{ github.workspace }} GOPATH: ${{ github.workspace }}
GO111MODULE: off GO111MODULE: off
steps: steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -101,7 +133,7 @@ jobs:
name: Set up Go name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: "1.27.4" go-version: "1.26.8"
cache: false cache: false
- -
name: Run gocompat check name: Run gocompat check
+11 -1
View File
@@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics. # 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 # TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.27.4" go: "1.26.8"
timeout: 5m timeout: 5m
@@ -201,6 +201,11 @@ linters:
linters: linters:
- gosec - gosec
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "package-comments: should have a package comment"
linters:
- revive
# Exclude some linters from running on tests files. # Exclude some linters from running on tests files.
- path: _test\.go - path: _test\.go
linters: linters:
@@ -222,6 +227,11 @@ linters:
linters: linters:
- staticcheck - staticcheck
# TODO(thaJeztah): remove once https://github.com/leighmcculloch/gocheckcompilerdirectives/issues/7 is fixed.
- text: "compiler directive unrecognized: //go:fix"
linters:
- gocheckcompilerdirectives
# Log a warning if an exclusion rule is unused. # Log a warning if an exclusion rule is unused.
# Default: false # Default: false
warn-unused: true warn-unused: true
+7 -9
View File
@@ -5,16 +5,14 @@ ARG BASE_VARIANT=alpine
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # 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 # 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. # that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.24 ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm
# BASE_DEBIAN_DISTRO sets the version of the debian base image to use, ARG GO_VERSION=1.26.8
ARG BASE_DEBIAN_DISTRO=trixie
ARG GO_VERSION=1.27.4
# XX_VERSION specifies the version of the xx utility to use. # 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. # It must be a valid tag in the docker.io/tonistiigi/xx image repository.
ARG XX_VERSION=1.10.0 ARG XX_VERSION=1.9.0
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install. # GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo # It must be a valid tag from https://github.com/josephspurrier/goversioninfo
@@ -27,12 +25,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
# BUILDX_VERSION sets the version of buildx to use for the e2e tests. # 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 # It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub. # on Docker Hub.
ARG BUILDX_VERSION=0.38.2 ARG BUILDX_VERSION=0.34.1
# COMPOSE_VERSION is the version of compose to install in the dev container. # 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 # It must be a tag in the docker.io/docker/compose-bin image repository
# on Docker Hub. # on Docker Hub.
ARG COMPOSE_VERSION=v5.7.0 ARG COMPOSE_VERSION=v5.1.3
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
@@ -55,7 +53,7 @@ WORKDIR /go/src/github.com/docker/cli
FROM build-base-debian AS build-debian FROM build-base-debian AS build-debian
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-14-dev pkgconf RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-12-dev pkgconf
FROM build-base-${BASE_VARIANT} AS goversioninfo FROM build-base-${BASE_VARIANT} AS goversioninfo
ARG GOVERSIONINFO_VERSION ARG GOVERSIONINFO_VERSION
+1
View File
@@ -5,6 +5,7 @@
[![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest) [![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli) [![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
[![Protected by Tracebit Community Edition](https://github.com/tracebit-com/tracebit-community-action/blob/main/assets/badges/tracebit-badge-compact.svg?raw=1)](https://community.tracebit.com/supplychainattacks)
## About ## About
+1 -1
View File
@@ -1 +1 @@
29.10.0 29.8.2
-4
View File
@@ -30,11 +30,7 @@ package hooks
type ResponseType int type ResponseType int
const ( const (
// NextSteps renders suggestions under a shared "What's next:" header.
NextSteps ResponseType = 0 NextSteps ResponseType = 0
// GenericMessage renders before NextSteps, without a header or indentation.
// CLI versions that do not recognize this type ignore the response.
GenericMessage ResponseType = 1
) )
// Request is the type representing the information // Request is the type representing the information
+10 -35
View File
@@ -7,42 +7,17 @@ const (
indent = " " indent = " "
) )
// PrintMessages groups messages by type and writes them to out. Generic messages
// appear first, without a header or indentation, followed by next steps under a
// shared "What's next:" header. Order within each type is preserved.
// Messages with no lines or an unknown type produce no output.
func PrintMessages(out io.Writer, messages []Response, evaluate func(Response) []string) {
grouped := make(map[ResponseType][]string)
for _, message := range messages {
lines := evaluate(message)
grouped[message.Type] = append(grouped[message.Type], lines...)
}
for _, responseType := range []ResponseType{GenericMessage, NextSteps} {
lines := grouped[responseType]
if len(lines) == 0 {
continue
}
header, prefix := "\n", ""
if responseType == NextSteps {
header, prefix = whatsNext, indent
}
_, _ = io.WriteString(out, header)
for _, line := range lines {
_, _ = io.WriteString(out, prefix)
_, _ = io.WriteString(out, line)
_, _ = io.WriteString(out, "\n")
}
}
}
// PrintNextSteps renders list of [NextSteps] messages and writes them // PrintNextSteps renders list of [NextSteps] messages and writes them
// to out. It is a no-op if messages is empty. // to out. It is a no-op if messages is empty.
//
// Deprecated: use [PrintMessages] instead.
func PrintNextSteps(out io.Writer, messages []string) { func PrintNextSteps(out io.Writer, messages []string) {
PrintMessages(out, []Response{{Type: NextSteps}}, func(Response) []string { if len(messages) == 0 {
return messages return
}) }
_, _ = io.WriteString(out, whatsNext)
for _, msg := range messages {
_, _ = io.WriteString(out, indent)
_, _ = io.WriteString(out, msg)
_, _ = io.WriteString(out, "\n")
}
} }
+1 -84
View File
@@ -8,85 +8,7 @@ import (
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
func TestPrintMessages(t *testing.T) { func TestPrintHookMessages(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
for _, tc := range []struct {
doc string
messages []hooks.Response
expected string
}{
{doc: "no messages"},
{
doc: "no lines",
messages: []hooks.Response{
{Type: hooks.GenericMessage},
{Type: hooks.NextSteps},
},
},
{
doc: "generic message",
messages: []hooks.Response{
{Type: hooks.GenericMessage, Template: "Session active"},
},
expected: "\nSession active\n",
},
{
doc: "preserve message formatting",
messages: []hooks.Response{
{Type: hooks.GenericMessage, Template: "Session active\n\n Details"},
},
expected: "\nSession active\n\n Details\n",
},
{
doc: "next steps",
messages: []hooks.Response{
{Type: hooks.NextSteps, Template: "Try another command"},
},
expected: header + " Try another command\n",
},
{
doc: "group interleaved types and preserve order within each type",
messages: []hooks.Response{
{Type: hooks.NextSteps, Template: "First suggestion"},
{Type: hooks.GenericMessage, Template: "Session active"},
{Type: hooks.NextSteps, Template: "Second suggestion\nThird suggestion"},
{Type: hooks.GenericMessage, Template: "Another status"},
},
expected: "\nSession active\nAnother status\n" + header + " First suggestion\n Second suggestion\n Third suggestion\n",
},
{
doc: "unknown type",
messages: []hooks.Response{
{Type: hooks.ResponseType(99), Template: "Ignored"},
},
},
{
doc: "unknown type does not affect known messages",
messages: []hooks.Response{
{Type: hooks.ResponseType(99), Template: "Ignored"},
{Type: hooks.GenericMessage, Template: "Session active"},
{Type: hooks.NextSteps, Template: "Try another command"},
},
expected: "\nSession active\n" + header + " Try another command\n",
},
} {
t.Run(tc.doc, func(t *testing.T) {
var w strings.Builder
hooks.PrintMessages(&w, tc.messages, func(message hooks.Response) []string {
if message.Template == "" {
return nil
}
lines, err := hooks.ParseTemplate(message.Template, nil)
assert.NilError(t, err)
return lines
})
assert.Equal(t, w.String(), tc.expected)
})
}
}
func TestPrintNextSteps(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n" const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
tests := []struct { tests := []struct {
@@ -112,11 +34,6 @@ func TestPrintNextSteps(t *testing.T) {
" Foo\n" + " Foo\n" +
" bar\n", " bar\n",
}, },
{
doc: "preserve empty and indented lines",
messages: []string{"", " Details", ""},
expectedOutput: header + " \n Details\n \n",
},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
+1 -11
View File
@@ -7,7 +7,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"slices"
"strings" "strings"
"text/template" "text/template"
@@ -44,16 +43,7 @@ func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
if n := strings.Count(out, "\n"); n > maxMessages { if n := strings.Count(out, "\n"); n > maxMessages {
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages) return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
} }
lines := strings.SplitN(out, "\n", maxMessages) return strings.SplitN(out, "\n", maxMessages), nil
// Skip messages containing only whitespace, but preserve empty lines
// in messages that have content (e.g., for paragraph spacing).
if !slices.ContainsFunc(lines, func(line string) bool {
return strings.TrimSpace(line) != ""
}) {
return nil, nil
}
return lines, nil
} }
var ErrHookTemplateParse = errors.New("failed to parse hook template") var ErrHookTemplateParse = errors.New("failed to parse hook template")
+1 -11
View File
@@ -33,17 +33,7 @@ func TestParseTemplate(t *testing.T) {
{ {
doc: "empty template", doc: "empty template",
template: "", template: "",
expectedOutput: nil, expectedOutput: []string{""},
},
{
doc: "only empty lines",
template: "\n\n",
expectedOutput: nil,
},
{
doc: "whitespace-only lines",
template: " \n\t\n ",
expectedOutput: nil,
}, },
{ {
doc: "plain message", doc: "plain message",
+47 -23
View File
@@ -49,18 +49,11 @@ func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, sub
} }
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) { func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
messages := invokeAndCollectHooks(ctx, cfg, rootCmd, invokedCommand, flags, cmdErrorMessage) nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintMessages(subCommand.ErrOrStderr(), messages, func(message hooks.Response) []string { hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
lines, err := hooks.ParseTemplate(message.Template, subCommand)
if err != nil {
logrus.WithError(err).Debug("Failed to evaluate hook template")
return nil
}
return lines
})
} }
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []hooks.Response { 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 { if ctx.Err() != nil {
return nil return nil
} }
@@ -71,17 +64,17 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
} }
pluginDirs := getPluginDirs(cfg) pluginDirs := getPluginDirs(cfg)
var collected []hooks.Response nextSteps := make([]string, 0, len(pluginsCfg))
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (hooks.Response, error) { tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage) match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
if !matched { if !matched {
return hooks.Response{}, nil return nil, false, nil
} }
p, err := getPlugin(pluginName, pluginDirs, rootCmd) p, err := getPlugin(pluginName, pluginDirs, rootCmd)
if err != nil { if err != nil {
return hooks.Response{}, err return nil, false, err
} }
resp, err := p.RunHook(ctx, hooks.Request{ resp, err := p.RunHook(ctx, hooks.Request{
@@ -90,23 +83,29 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
CommandError: cmdErrorMessage, CommandError: cmdErrorMessage,
}) })
if err != nil { if err != nil {
return hooks.Response{}, err return nil, false, err
} }
var message hooks.Response var message hooks.Response
if err := json.Unmarshal(resp, &message); err != nil { if err := json.Unmarshal(resp, &message); err != nil {
return hooks.Response{}, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err) return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
} }
if message.Type != hooks.NextSteps && message.Type != hooks.GenericMessage { // currently the only hook type
return hooks.Response{}, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type))) if message.Type != hooks.NextSteps {
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
} }
return message, nil messages, err = hooks.ParseTemplate(message.Template, subCmd)
if err != nil {
return nil, false, err
}
return messages, true, nil
} }
for pluginName, pluginCfg := range pluginsCfg { for pluginName, pluginCfg := range pluginsCfg {
message, err := tryInvokeHook(pluginName, pluginCfg) messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
if err != nil { if err != nil {
// skip misbehaving plugins, but don't halt execution // skip misbehaving plugins, but don't halt execution
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
@@ -115,13 +114,38 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
}).Debug("Plugin hook invocation failed") }).Debug("Plugin hook invocation failed")
continue continue
} }
if message.Template == "" { if !ok {
continue continue
} }
collected = append(collected, message) var appended bool
nextSteps, appended = appendNextSteps(nextSteps, messages)
if !appended {
logrus.WithFields(logrus.Fields{
"plugin": pluginName,
}).Debug("Plugin responded with an empty hook message; ignoring")
}
} }
return collected return nextSteps
}
// appendNextSteps appends the processed hook output to the nextSteps slice.
// If the processed hook output is empty, it is not appended.
// Empty lines are not stripped if there's at least one non-empty line.
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
empty := true
for _, l := range processed {
if strings.TrimSpace(l) != "" {
empty = false
break
}
}
if empty {
return nextSteps, false
}
return append(nextSteps, processed...), true
} }
// pluginMatch takes a plugin configuration and a string representing the // pluginMatch takes a plugin configuration and a string representing the
+35 -108
View File
@@ -2,11 +2,6 @@ package manager
import ( import (
"context" "context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing" "testing"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
@@ -263,6 +258,38 @@ func TestMatchHookConfig(t *testing.T) {
} }
} }
func TestAppendNextSteps(t *testing.T) {
testCases := []struct {
processed []string
expectedOut []string
}{
{
processed: []string{},
expectedOut: []string{},
},
{
processed: []string{"", ""},
expectedOut: []string{},
},
{
processed: []string{"Some hint", "", "Some other hint"},
expectedOut: []string{"Some hint", "", "Some other hint"},
},
{
processed: []string{"Hint 1", "Hint 2"},
expectedOut: []string{"Hint 1", "Hint 2"},
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
got, appended := appendNextSteps([]string{}, tc.processed)
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
assert.Check(t, is.Equal(appended, len(got) > 0))
})
}
}
func TestRunPluginHooksPassesErrorMessage(t *testing.T) { func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
cfg := configfile.New("") cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{ cfg.Plugins = map[string]map[string]string{
@@ -309,7 +336,7 @@ func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
// On success, error-hooks should not match, so the plugin // On success, error-hooks should not match, so the plugin
// binary is never looked up and no results are returned. // binary is never looked up and no results are returned.
result := invokeAndCollectHooks( result := invokeAndCollectHooks(
context.Background(), cfg, root, context.Background(), cfg, root, sub,
"build", map[string]string{}, "", "build", map[string]string{}, "",
) )
assert.Check(t, is.Len(result, 0)) assert.Check(t, is.Len(result, 0))
@@ -322,7 +349,7 @@ func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
root.AddCommand(sub) root.AddCommand(sub)
result := invokeAndCollectHooks( result := invokeAndCollectHooks(
context.Background(), cfg, root, context.Background(), cfg, root, sub,
"build", map[string]string{}, "some error", "build", map[string]string{}, "some error",
) )
assert.Check(t, is.Len(result, 0)) assert.Check(t, is.Len(result, 0))
@@ -341,108 +368,8 @@ func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
cancel() // cancel immediately cancel() // cancel immediately
result := invokeAndCollectHooks( result := invokeAndCollectHooks(
ctx, cfg, root, ctx, cfg, root, sub,
"build", map[string]string{}, "exit status 1", "build", map[string]string{}, "exit status 1",
) )
assert.Check(t, is.Nil(result)) assert.Check(t, is.Nil(result))
} }
func TestRunHooksMessages(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test plugins require a POSIX shell")
}
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
for _, tc := range []struct {
doc string
responses map[string]string
expected string
}{
{
doc: "generic message preserves formatting and expands templates",
responses: map[string]string{
"status": `{"Type":1,"Template":"Status for {{command}}\n\n Details"}`,
},
expected: "\nStatus for build\n\n Details\n",
},
{
doc: "omitted type defaults to next steps",
responses: map[string]string{
"suggestion": `{"Template":"Try another command"}`,
},
expected: header + " Try another command\n",
},
{
doc: "generic messages precede shared next steps header",
responses: map[string]string{
"statusone": `{"Type":1,"Template":"Session active"}`,
"statustwo": `{"Type":1,"Template":"Session active"}`,
"suggestionone": `{"Type":0,"Template":"Try another command"}`,
"suggestiontwo": `{"Type":0,"Template":"Try another command"}`,
},
expected: "\nSession active\nSession active\n" + header + " Try another command\n Try another command\n",
},
{
doc: "empty responses produce no output",
responses: map[string]string{
"status": `{"Type":1,"Template":" \n\t"}`,
"suggestion": `{"Type":0,"Template":""}`,
},
},
{
doc: "empty generic message does not affect next steps",
responses: map[string]string{
"status": `{"Type":1,"Template":""}`,
"suggestion": `{"Type":0,"Template":"Try another command"}`,
},
expected: header + " Try another command\n",
},
{
doc: "invalid responses do not affect other plugins",
responses: map[string]string{
"unknown": `{"Type":99,"Template":"Ignored"}`,
"invalid": `{"Type":1,"Template":"{{"}`,
"status": `{"Type":1,"Template":"Session active"}`,
"suggestion": `{"Type":0,"Template":"Try another command"}`,
},
expected: "\nSession active\n" + header + " Try another command\n",
},
} {
t.Run(tc.doc, func(t *testing.T) {
pluginDir := t.TempDir()
cfg := configfile.New("")
cfg.CLIPluginsExtraDirs = []string{pluginDir}
cfg.Plugins = make(map[string]map[string]string)
for name, response := range tc.responses {
plugin := fmt.Sprintf(`#!/bin/sh
case "$1" in
docker-cli-plugin-metadata)
printf '%%s\n' '{"SchemaVersion":"0.1.0","Vendor":"test"}'
;;
*)
test "$2" = docker-cli-plugin-hooks || exit 1
printf 'called\n' >> "$0.calls"
printf '%%s\n' '%s'
;;
esac
`, response)
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+name), []byte(plugin), 0o755))
cfg.Plugins[name] = map[string]string{"hooks": "build"}
}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
var output strings.Builder
sub.SetErr(&output)
runHooks(context.Background(), cfg, root, sub, "build", nil, "")
assert.Equal(t, output.String(), tc.expected)
for name := range tc.responses {
calls, err := os.ReadFile(filepath.Join(pluginDir, "docker-"+name+".calls"))
assert.NilError(t, err)
assert.Equal(t, string(calls), "called\n", "each plugin hook must run exactly once")
}
})
}
}
-6
View File
@@ -1,6 +1,3 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package metadata package metadata
const ( const (
@@ -38,7 +35,4 @@ type Metadata struct {
URL string `json:",omitempty"` URL string `json:",omitempty"`
// Hidden hides the plugin in completion and help message output. // Hidden hides the plugin in completion and help message output.
Hidden bool `json:",omitempty"` Hidden bool `json:",omitempty"`
// Features declares optional contracts supported by the plugin, keyed by
// feature name.
Features map[string]any `json:",omitempty"`
} }
+2 -62
View File
@@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"os" "os"
"sync" "sync"
"time"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/metadata"
@@ -33,11 +32,6 @@ import (
// called. // called.
var PersistentPreRunE func(*cobra.Command, []string) error var PersistentPreRunE func(*cobra.Command, []string) error
// finalizeHookTimeout is the timeout given for running the finalize hook.
// It contains the timeout for exporting metrics and a little bit extra in case
// shutdown does not respond to the export timeout promptly.
const finalizeHookTimeout = command.ExportTimeout + 10*time.Millisecond
// RunPlugin executes the specified plugin command // RunPlugin executes the specified plugin command
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error { func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta) tcmd := newPluginCommand(dockerCli, plugin, meta)
@@ -75,7 +69,6 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
stopInstrumentation(err) stopInstrumentation(err)
return err return err
} }
cobra.OnFinalize(func() { finalizeHook(dockerCli) })
}) })
return retErr return retErr
} }
@@ -90,59 +83,6 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
return cmd.Execute() return cmd.Execute()
} }
// finalizeHook is a hook to be run when the CLI has finished executing the run command.
// This happens after any post run methods but also always runs even when an error happens.
func finalizeHook(dockerCli *command.DockerCli) {
// Use a new context for the export so that the command being cancelled
// doesn't affect the metrics, and we get metrics for cancelled commands.
ctx, cancel := context.WithTimeout(context.Background(), command.ExportTimeout)
defer cancel()
var (
wg sync.WaitGroup
wait bool
)
if tp, ok := dockerCli.TracerProvider().(command.TracerProvider); ok {
wg.Go(func() {
if err := tp.Shutdown(ctx); err != nil {
otel.Handle(err)
}
})
wait = true
}
if mp, ok := dockerCli.MeterProvider().(command.MeterProvider); ok {
wg.Go(func() {
if err := mp.Shutdown(ctx); err != nil {
otel.Handle(err)
}
})
wait = true
}
// Wait for the wait group in another goroutine if any were created.
if !wait {
return
}
// Spawn a goroutine to wait for the wait group.
// If the shutdown does not promptly hang up with the
// cancellation of the context, we leave this goroutine
// orphaned so the process can exit.
done := make(chan struct{})
go func() {
defer close(done)
wg.Wait()
}()
select {
case <-done:
case <-time.After(finalizeHookTimeout):
// Ensure we still exit even if the shutdown doesn't work properly.
// We give 10 extra milliseconds for the cleanup to happen.
}
}
// Run is the top-level entry point to the CLI plugin framework. It should // 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 // be called from the plugin's "main()" function. It initializes a new
// [command.DockerCli] instance with the given options before calling // [command.DockerCli] instance with the given options before calling
@@ -272,8 +212,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra
Use: metadata.MetadataSubcommandName, Use: metadata.MetadataSubcommandName,
Hidden: true, Hidden: true,
// Suppress the global/parent PersistentPreRunE, which // Suppress the global/parent PersistentPreRunE, which
// needlessly initializes the client and tries to connect to the // needlessly initializes the client and tries to
// daemon. // connect to the daemon.
PersistentPreRun: func(cmd *cobra.Command, args []string) {}, PersistentPreRun: func(cmd *cobra.Command, args []string) {},
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
enc := json.NewEncoder(os.Stdout) enc := json.NewEncoder(os.Stdout)
+1 -19
View File
@@ -160,25 +160,7 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro
return nil, nil, cmd.FlagErrorFunc()(cmd, err) return nil, nil, cmd.FlagErrorFunc()(cmd, err)
} }
args := flags.Args() return cmd, flags.Args(), nil
// A leading "--help"/"-h" is consumed above as the root's persistent
// help flag and stripped from args, so the target subcommand never sees
// it. That's fine for most subcommands, which inherit the same help
// flag instance, but "run" and "create" register their own local
// "help" flag (to avoid a shorthand clash with "-h"/"--hostname"),
// which shadows the persistent one and is left false. Propagate the
// value so "docker --help run <image>" shows help like "docker run
// --help" does, instead of running the container.
if help, _ := flags.GetBool("help"); help {
if sub, _, err := cmd.Find(args); err == nil && sub != cmd {
if sub.Flags().Lookup("help") != nil {
_ = sub.Flags().Set("help", "true")
}
}
}
return cmd, args, nil
} }
// Initialize finalises global option parsing and initializes the docker client. // Initialize finalises global option parsing and initializes the docker client.
-42
View File
@@ -108,45 +108,3 @@ func TestDecoratedName(t *testing.T) {
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"} topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"}
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*") assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
} }
// TestHandleGlobalFlagsHelpPropagation covers https://github.com/docker/cli/issues/7473:
// a leading "--help" is consumed as the root's persistent help flag and
// stripped from args, so a subcommand that registers its own local "help"
// flag (as "run" and "create" do, to avoid a shorthand clash with "-h") never
// sees it and runs instead of showing help.
func TestHandleGlobalFlagsHelpPropagation(t *testing.T) {
newRootCmd := func() (root, sub *cobra.Command) {
root = &cobra.Command{Use: "docker"}
root.PersistentFlags().BoolP("help", "h", false, "Print usage")
sub = &cobra.Command{Use: "run", RunE: func(cmd *cobra.Command, args []string) error { return nil }}
sub.Flags().Bool("help", false, "Print usage")
root.AddCommand(sub)
return root, sub
}
t.Run("propagates to the shadowing local help flag", func(t *testing.T) {
root, sub := newRootCmd()
tcmd := &TopLevelCommand{cmd: root, args: []string{"--help", "run", "hello-world"}}
_, args, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
assert.DeepEqual(t, args, []string{"run", "hello-world"})
help, err := sub.Flags().GetBool("help")
assert.NilError(t, err)
assert.Equal(t, help, true)
})
t.Run("leaves the local help flag alone without --help", func(t *testing.T) {
root, sub := newRootCmd()
tcmd := &TopLevelCommand{cmd: root, args: []string{"run", "hello-world"}}
_, _, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
help, err := sub.Flags().GetBool("help")
assert.NilError(t, err)
assert.Equal(t, help, false)
})
}
-13
View File
@@ -68,7 +68,6 @@ type DockerCli struct {
serverInfo ServerInfo serverInfo ServerInfo
contextStore store.Store contextStore store.Store
currentContext string currentContext string
contextResolver func(*DockerCli) (string, error)
init sync.Once init sync.Once
initErr error initErr error
dockerEndpoint docker.Endpoint dockerEndpoint docker.Endpoint
@@ -209,8 +208,6 @@ func (cli *DockerCli) HooksEnabled() bool {
// Initialize the dockerCli runs initialization that must happen after command // Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed. // line flags are parsed.
//
//nolint:gocyclo
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
for _, o := range ops { for _, o := range ops {
if err := o(cli); err != nil { if err := o(cli); err != nil {
@@ -260,16 +257,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
}, },
} }
if cli.contextResolver != nil {
contextName, err := cli.contextResolver(cli)
if err != nil {
return err
}
if contextName != "" {
cli.currentContext = contextName
}
}
// TODO(krissetto): pass ctx to the funcs instead of using this // TODO(krissetto): pass ctx to the funcs instead of using this
if cli.enableGlobalMeter { if cli.enableGlobalMeter {
cli.createGlobalMeterProvider(cli.baseCtx) cli.createGlobalMeterProvider(cli.baseCtx)
-13
View File
@@ -84,19 +84,6 @@ func WithDefaultContextStoreConfig() CLIOption {
} }
} }
// WithContextResolver overrides context selection during [DockerCli.Initialize].
// The resolver runs after configuration and the context store are loaded, but
// before telemetry and endpoint initialization.
// An empty result preserves normal context selection; an error aborts
// initialization.
// The resolver must not initialize the API client.
func WithContextResolver(resolve func(*DockerCli) (string, error)) CLIOption {
return func(cli *DockerCli) error {
cli.contextResolver = resolve
return nil
}
}
// WithAPIClient configures the cli to use the given API client. // WithAPIClient configures the cli to use the given API client.
func WithAPIClient(c client.APIClient) CLIOption { func WithAPIClient(c client.APIClient) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
-58
View File
@@ -325,64 +325,6 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
assert.Check(t, cli.ContextStore() != nil) assert.Check(t, cli.ContextStore() != nil)
} }
func TestInitializeContextResolver(t *testing.T) {
resolverErr := errors.New("resolver failed")
for _, tc := range []struct {
name string
context string
err error
}{
{name: "resolved context", context: "resolved"},
{name: "normal selection"},
{name: "resolver failure", err: resolverErr},
} {
t.Run(tc.name, func(t *testing.T) {
originalConfigDir := config.Dir()
t.Cleanup(func() { config.SetDir(originalConfigDir) })
config.SetDir(t.TempDir())
configDir := t.TempDir()
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
cfg.Features = map[string]string{"cloud": "foobar"}
assert.NilError(t, cfg.Save())
cli, err := NewDockerCli()
assert.NilError(t, err)
var loadedConfig *configfile.ConfigFile
calls := 0
err = cli.Initialize(&flags.ClientOptions{ConfigDir: configDir, Context: "original"},
WithContextResolver(func(cli *DockerCli) (string, error) {
calls++
assert.Equal(t, config.Dir(), configDir)
loadedConfig = cli.ConfigFile()
assert.Equal(t, loadedConfig.Features["cloud"], "foobar")
assert.Assert(t, cli.ContextStore() != nil)
assert.Assert(t, cli.client == nil)
return tc.context, tc.err
}),
)
assert.Equal(t, calls, 1)
assert.Assert(t, cli.ConfigFile() == loadedConfig)
assert.Assert(t, cli.client == nil)
if tc.err != nil {
assert.ErrorIs(t, err, tc.err)
return
}
assert.NilError(t, err)
wantContext := tc.context
if wantContext == "" {
wantContext = "original"
}
assert.Equal(t, cli.CurrentContext(), wantContext)
})
}
}
func TestHooksEnabled(t *testing.T) { func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) { t.Run("disabled by default", func(t *testing.T) {
// Make sure we don't depend on any existing ~/.docker/config.json // Make sure we don't depend on any existing ~/.docker/config.json
+10 -16
View File
@@ -335,27 +335,23 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
} }
restore, done := copyProgress(ctx, dockerCLI.Err(), copyFromContainerHeader, &copiedSize) restore, done := copyProgress(ctx, dockerCLI.Err(), copyFromContainerHeader, &copiedSize)
err = archive.CopyTo(preArchive, srcInfo, dstPath) res := archive.CopyTo(preArchive, srcInfo, dstPath)
cancel() cancel()
<-done <-done
restore() restore()
if err == nil { reportedSize := copiedSize
reportedSize := copiedSize if !cpRes.Stat.Mode.IsDir() {
if !cpRes.Stat.Mode.IsDir() { reportedSize = cpRes.Stat.Size
reportedSize = cpRes.Stat.Size
}
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, dstPath))
} }
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, dstPath))
return err return res
} }
// In order to get the copy behavior right, we need to know information // In order to get the copy behavior right, we need to know information
// about both the source and destination. The API is a simple tar // 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 // archive/extract API but we can use the stat info header about the
// destination to be more informed about exactly what the destination is. // destination to be more informed about exactly what the destination is.
//
//nolint:gocyclo
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) error { func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) error {
srcPath := copyConfig.sourcePath srcPath := copyConfig.sourcePath
dstPath := copyConfig.destPath dstPath := copyConfig.destPath
@@ -478,13 +474,11 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
cancel() cancel()
<-done <-done
restore() restore()
if err == nil { reportedSize := copiedSize
reportedSize := copiedSize if sizeErr == nil {
if sizeErr == nil { reportedSize = contentSize
reportedSize = contentSize
}
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, copyConfig.container+":"+dstInfo.Path))
} }
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, copyConfig.container+":"+dstInfo.Path))
return err return err
} }
-62
View File
@@ -2,7 +2,6 @@ package container
import ( import (
"context" "context"
"errors"
"io" "io"
"os" "os"
"runtime" "runtime"
@@ -447,64 +446,3 @@ func TestCopyToContainerStdinReportsTransferredSize(t *testing.T) {
// "(transferred ...)" should not appear. // "(transferred ...)" should not appear.
assert.Check(t, !strings.Contains(errOut, "(transferred")) assert.Check(t, !strings.Contains(errOut, "(transferred"))
} }
func TestCopyFromContainerDoesNotReportSuccessOnFailure(t *testing.T) {
// Copying a directory onto an existing regular file fails while
// extracting ("cannot copy directory"). The failure must not be
// preceded by a "Successfully copied" message.
srcDir := fs.NewDir(t, "cp-test-fromdir-fail",
fs.WithFile("file1", "hello"))
destFile := fs.NewFile(t, "cp-test-fromdir-fail-dest", fs.WithContent("original\n"))
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: destFile.Path(),
})
assert.ErrorContains(t, err, "cannot copy directory")
assert.Check(t, !strings.Contains(fakeCli.ErrBuffer().String(), "Successfully copied"),
"unexpected success message in output: %q", fakeCli.ErrBuffer().String())
content, err := os.ReadFile(destFile.Path())
assert.NilError(t, err)
assert.Check(t, is.Equal("original\n", string(content)))
}
func TestCopyToContainerDoesNotReportSuccessOnFailure(t *testing.T) {
// A failed upload must not be followed by a "Successfully copied"
// message.
srcFile := fs.NewFile(t, "cp-test-to-fail", 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{}, errors.New("copy to container failed")
},
})
err := runCopy(context.TODO(), fakeCli, copyOptions{
source: srcFile.Path(),
destination: "container:/tmp",
})
assert.ErrorContains(t, err, "copy to container failed")
assert.Check(t, !strings.Contains(fakeCli.ErrBuffer().String(), "Successfully copied"),
"unexpected success message in output: %q", fakeCli.ErrBuffer().String())
}
+13 -16
View File
@@ -146,27 +146,24 @@ func (c *ContainerContext) ID() string {
// slash (/) prefix stripped. Additional names for the container (related to the // slash (/) prefix stripped. Additional names for the container (related to the
// legacy `--link` feature) are omitted when formatting "truncated". // legacy `--link` feature) are omitted when formatting "truncated".
func (c *ContainerContext) Names() string { func (c *ContainerContext) Names() string {
if c.trunc {
// When printing truncated, we only print a single name.
//
// Pick the first name that's not a legacy link (does not have
// slashes inside the name itself (e.g., "/other-container/link")).
// Normally this would be the first name found.
for _, n := range c.c.Names {
if name := strings.TrimPrefix(n, "/"); strings.IndexByte(name, '/') == -1 {
return name
}
}
// No name without slashes was found; this can happen when connected
// to a classic Swarm API, which prefixes names with the node-name
// ("/<node-name>/<container-name>"). Fall back to printing all names.
}
var b strings.Builder var b strings.Builder
for i, n := range c.c.Names { for i, n := range c.c.Names {
name := strings.TrimPrefix(n, "/")
if c.trunc {
// When printing truncated, we only print a single name.
//
// Pick the first name that's not a legacy link (does not have
// slashes inside the name itself (e.g., "/other-container/link")).
// Normally this would be the first name found.
if strings.IndexByte(name, '/') == -1 {
return name
}
continue
}
if i > 0 { if i > 0 {
b.WriteByte(',') b.WriteByte(',')
} }
b.WriteString(strings.TrimPrefix(n, "/")) b.WriteString(name)
} }
return b.String() return b.String()
} }
-20
View File
@@ -48,25 +48,6 @@ func TestContainerPsContext(t *testing.T) {
expValue: "foobar_baz", expValue: "foobar_baz",
call: ctx.Names, call: ctx.Names,
}, },
{
container: container.Summary{Names: []string{"/other/link", "/foobar_baz"}},
trunc: true,
expValue: "foobar_baz",
call: ctx.Names,
},
{
container: container.Summary{Names: []string{"/foobar_baz", "/other/link"}},
expValue: "foobar_baz,other/link",
call: ctx.Names,
},
{
// Names returned by a classic Swarm API are prefixed with the
// node-name; see https://github.com/docker/cli/pull/6875
container: container.Summary{Names: []string{"/node-1/foobar_baz"}},
trunc: true,
expValue: "node-1/foobar_baz",
call: ctx.Names,
},
{ {
container: container.Summary{Image: "ubuntu"}, container: container.Summary{Image: "ubuntu"},
trunc: true, trunc: true,
@@ -276,7 +257,6 @@ func TestContainerPsContext(t *testing.T) {
} }
} }
//nolint:dupword // ignore duplicate "ubuntu" in test fixtures
func TestContainerContextWrite(t *testing.T) { func TestContainerContextWrite(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -1).Unix() unixTime := time.Now().AddDate(0, 0, -1).Unix()
expectedTime := time.Unix(unixTime, 0).String() expectedTime := time.Unix(unixTime, 0).String()
+2 -5
View File
@@ -260,11 +260,8 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
contextDir = tempDir contextDir = tempDir
case build.ContextTypeRemote: case build.ContextTypeRemote:
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName) buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName)
if err != nil { if err != nil && options.quiet {
if options.quiet { _, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
}
return err
} }
default: default:
return fmt.Errorf("unable to prepare context: path %q not found", options.context) return fmt.Errorf("unable to prepare context: path %q not found", options.context)
-31
View File
@@ -6,8 +6,6 @@ import (
"compress/gzip" "compress/gzip"
"context" "context"
"io" "io"
"net/http"
"net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
@@ -174,35 +172,6 @@ RUN echo hello world
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"}) assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
} }
// TestRunBuildRemoteContextDownloadError verifies that remote context
// download errors are propagated.
//
// regression test for https://github.com/docker/cli/issues/7372
func TestRunBuildRemoteContextDownloadError(t *testing.T) {
t.Setenv("DOCKER_BUILDKIT", "0")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not found", http.StatusNotFound)
}))
defer server.Close()
imageBuildCalled := false
cli := test.NewFakeCli(&fakeClient{
imageBuildFunc: func(ctx context.Context, buildContext io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) {
imageBuildCalled = true
return client.ImageBuildResult{Body: io.NopCloser(bytes.NewReader(nil))}, nil
},
})
options := newBuildOptions()
options.context = server.URL
err := runBuild(context.Background(), cli, options)
assert.ErrorContains(t, err, "unable to download remote context")
assert.Assert(t, !imageBuildCalled, "ImageBuild should not be called when downloading the remote context fails")
}
type fakeBuild struct { type fakeBuild struct {
context *tar.Reader context *tar.Reader
options client.ImageBuildOptions options client.ImageBuildOptions
+2 -30
View File
@@ -10,10 +10,8 @@ import (
"slices" "slices"
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/manifest/store" "github.com/docker/cli/cli/manifest/store"
"github.com/docker/cli/internal/registryclient" "github.com/docker/cli/internal/registryclient"
@@ -22,23 +20,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var (
// similar to https://github.com/containerd/platforms/blob/v1.0.0-rc.5/database.go#L29-L38
knownOSes = []string{
"aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos",
"ios", "js", "linux", "nacl", "netbsd", "openbsd", "plan9", "solaris",
"windows", "zos",
}
// similar to https://github.com/containerd/platforms/blob/v1.0.0-rc.5/database.go#L51-L60
knownArches = []string{
"386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64",
"ppc64le", "loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32",
"mips64p32le", "ppc", "riscv", "riscv64", "s390", "s390x", "sparc",
"sparc64", "wasm",
}
)
type annotateOptions struct { type annotateOptions struct {
target string // the target manifest list name (also transaction ID) target string // the target manifest list name (also transaction ID)
image string // the manifest to annotate within the list image string // the manifest to annotate within the list
@@ -116,8 +97,6 @@ func newAnnotateCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Set operating system feature") flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Set operating system feature")
flags.StringVar(&opts.variant, "variant", "", "Set architecture variant") flags.StringVar(&opts.variant, "variant", "", "Set architecture variant")
_ = cmd.RegisterFlagCompletionFunc("os", completion.FromList(knownOSes...))
_ = cmd.RegisterFlagCompletionFunc("arch", completion.FromList(knownArches...))
return cmd return cmd
} }
@@ -160,15 +139,8 @@ func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error {
imageManifest.Descriptor.Platform.OSVersion = opts.osVersion imageManifest.Descriptor.Platform.OSVersion = opts.osVersion
} }
// Validate platform syntax, without restricting OS/architecture combinations. if !isValidOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture) {
// return fmt.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
// TODO(thaJeztah): Implement a platforms.Validate in containerd/platforms and replace this code.
p := imageManifest.Descriptor.Platform
if p.OS == "" || p.Architecture == "" {
return fmt.Errorf("manifest entry for image has missing os/architecture: %q/%q", p.OS, p.Architecture)
}
if _, err := platforms.Parse(platforms.FormatAll(*p)); err != nil {
return fmt.Errorf("manifest entry for image has invalid platform: %w", err)
} }
return manifestStore.Save(targetRef, imgRef, imageManifest) return manifestStore.Save(targetRef, imgRef, imageManifest)
} }
+7 -8
View File
@@ -58,16 +58,15 @@ func TestManifestAnnotate(t *testing.T) {
assert.ErrorContains(t, cmd.Execute(), expectedError) assert.ErrorContains(t, cmd.Execute(), expectedError)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
assert.NilError(t, cmd.Flags().Set("os", "freebsd")) cmd.Flags().Set("os", "freebsd")
assert.NilError(t, cmd.Flags().Set("arch", "<invalid>")) cmd.Flags().Set("arch", "fake")
assert.NilError(t, cmd.Flags().Set("os-version", "1")) cmd.Flags().Set("os-version", "1")
assert.NilError(t, cmd.Flags().Set("os-features", "feature1")) cmd.Flags().Set("os-features", "feature1")
assert.NilError(t, cmd.Flags().Set("variant", "v7")) cmd.Flags().Set("variant", "v7")
expectedError = "manifest entry for image has unsupported os/arch combination"
expectedError = "manifest entry for image has invalid platform"
assert.ErrorContains(t, cmd.Execute(), expectedError) assert.ErrorContains(t, cmd.Execute(), expectedError)
assert.NilError(t, cmd.Flags().Set("arch", "arm")) cmd.Flags().Set("arch", "arm")
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
cmd = newInspectCommand(cli) cmd = newInspectCommand(cli)
+47
View File
@@ -9,6 +9,53 @@ import (
"github.com/docker/cli/cli/manifest/types" "github.com/docker/cli/cli/manifest/types"
) )
type osArch struct {
os string
arch string
}
// Remove any unsupported os/arch combo
// list of valid os/arch values (see "Optional Environment Variables" section
// of https://go.dev/doc/install/source
// Added linux/s390x as we know System z support already exists
// Keep in sync with _docker_manifest_annotate in contrib/completion/bash/docker
var validOSArches = map[osArch]bool{
{os: "darwin", arch: "386"}: true,
{os: "darwin", arch: "amd64"}: true,
{os: "darwin", arch: "arm"}: true,
{os: "darwin", arch: "arm64"}: true,
{os: "dragonfly", arch: "amd64"}: true,
{os: "freebsd", arch: "386"}: true,
{os: "freebsd", arch: "amd64"}: true,
{os: "freebsd", arch: "arm"}: true,
{os: "linux", arch: "386"}: true,
{os: "linux", arch: "amd64"}: true,
{os: "linux", arch: "arm"}: true,
{os: "linux", arch: "arm64"}: true,
{os: "linux", arch: "ppc64le"}: true,
{os: "linux", arch: "mips64"}: true,
{os: "linux", arch: "mips64le"}: true,
{os: "linux", arch: "riscv64"}: true,
{os: "linux", arch: "s390x"}: true,
{os: "netbsd", arch: "386"}: true,
{os: "netbsd", arch: "amd64"}: true,
{os: "netbsd", arch: "arm"}: true,
{os: "openbsd", arch: "386"}: true,
{os: "openbsd", arch: "amd64"}: true,
{os: "openbsd", arch: "arm"}: true,
{os: "plan9", arch: "386"}: true,
{os: "plan9", arch: "amd64"}: true,
{os: "solaris", arch: "amd64"}: true,
{os: "windows", arch: "386"}: true,
{os: "windows", arch: "amd64"}: true,
}
func isValidOSArch(os string, arch string) bool {
// check for existence of this combo
_, ok := validOSArches[osArch{os, arch}]
return ok
}
func normalizeReference(ref string) (reference.Named, error) { func normalizeReference(ref string) (reference.Named, error) {
namedRef, err := reference.ParseNormalizedNamed(ref) namedRef, err := reference.ParseNormalizedNamed(ref)
if err != nil { if err != nil {
+6 -6
View File
@@ -19,7 +19,6 @@ import (
"github.com/docker/cli/internal/tui" "github.com/docker/cli/internal/tui"
registrytypes "github.com/moby/moby/api/types/registry" registrytypes "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client" "github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@@ -227,7 +226,7 @@ func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, auth
return resp.Auth.Status, err return resp.Auth.Status, err
} }
func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (message string, _ error) { func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
// Some links documenting this: // Some links documenting this:
// - https://code.google.com/archive/p/mintty/issues/56 // - https://code.google.com/archive/p/mintty/issues/56
// - https://github.com/docker/docker/issues/15272 // - https://github.com/docker/docker/issues/15272
@@ -241,12 +240,13 @@ func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, de
// If we're logging into the index server and the user didn't provide a username or password, use the device flow // If we're logging into the index server and the user didn't provide a username or password, use the device flow
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" { if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" {
msg, err := loginWithDeviceCodeFlow(ctx, dockerCLI) var err error
if err == nil || !errors.Is(err, manager.ErrDeviceLoginStartFail) { msg, err = loginWithDeviceCodeFlow(ctx, dockerCLI)
// if the error represents a failure to initiate the device-code flow,
// then we fallback to regular cli credentials login
if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
return msg, err return msg, err
} }
logrus.WithError(err).Debug("failed to start web-based login")
_, _ = fmt.Fprint(dockerCLI.Err(), "Failed to start web-based login - falling back to command line login...\n\n") _, _ = fmt.Fprint(dockerCLI.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
} }
+2 -2
View File
@@ -210,9 +210,9 @@ func TestResourceOptionsToResourceRequirementsSwap(t *testing.T) {
r, err = swapOptions.ToResourceRequirements(flags) r, err = swapOptions.ToResourceRequirements(flags)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, r.SwapBytes != nil) assert.Check(t, r.SwapBytes != nil)
assert.Check(t, is.Equal(*r.SwapBytes, int64(86000))) assert.Check(t, is.Equal(*(r.SwapBytes), int64(86000)))
assert.Check(t, r.MemorySwappiness != nil) assert.Check(t, r.MemorySwappiness != nil)
assert.Check(t, is.Equal(*r.MemorySwappiness, int64(23))) assert.Check(t, is.Equal(*(r.MemorySwappiness), int64(23)))
} }
func TestToServiceNetwork(t *testing.T) { func TestToServiceNetwork(t *testing.T) {
+2 -2
View File
@@ -20,7 +20,7 @@ import (
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
const ExportTimeout = 50 * time.Millisecond const exportTimeout = 50 * time.Millisecond
// TracerProvider is an extension of the trace.TracerProvider interface for CLI programs. // TracerProvider is an extension of the trace.TracerProvider interface for CLI programs.
type TracerProvider interface { type TracerProvider interface {
@@ -205,7 +205,7 @@ func (r *cliReader) Shutdown(ctx context.Context) error {
// We don't want CLI metrics to prevent the CLI from exiting // We don't want CLI metrics to prevent the CLI from exiting
// so if there's some kind of issue we need to abort pretty // so if there's some kind of issue we need to abort pretty
// quickly. // quickly.
ctx, cancel := context.WithTimeout(ctx, ExportTimeout) ctx, cancel := context.WithTimeout(ctx, exportTimeout)
defer cancel() defer cancel()
return r.ForceFlush(ctx) return r.ForceFlush(ctx)
+1 -6
View File
@@ -99,10 +99,6 @@ func dockerSpanExporter(ctx context.Context, cli Cli) []sdktrace.TracerProviderO
opts := []otlptracegrpc.Option{ opts := []otlptracegrpc.Option{
otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithEndpoint(endpoint),
// The whole export is bounded by ExportTimeout, which is too short for a retry
// backoff to complete. With retries on, a refused connection (for example a stale
// Docker Desktop socket) waits out the full timeout instead of failing right away.
otlptracegrpc.WithRetry(otlptracegrpc.RetryConfig{Enabled: false}),
} }
if !secure { if !secure {
opts = append(opts, otlptracegrpc.WithInsecure()) opts = append(opts, otlptracegrpc.WithInsecure())
@@ -113,7 +109,7 @@ func dockerSpanExporter(ctx context.Context, cli Cli) []sdktrace.TracerProviderO
otel.Handle(err) otel.Handle(err)
return nil return nil
} }
return []sdktrace.TracerProviderOption{sdktrace.WithBatcher(exp, sdktrace.WithExportTimeout(ExportTimeout))} return []sdktrace.TracerProviderOption{sdktrace.WithBatcher(exp, sdktrace.WithExportTimeout(exportTimeout))}
} }
func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option { func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option {
@@ -124,7 +120,6 @@ func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option {
opts := []otlpmetricgrpc.Option{ opts := []otlpmetricgrpc.Option{
otlpmetricgrpc.WithEndpoint(endpoint), otlpmetricgrpc.WithEndpoint(endpoint),
otlpmetricgrpc.WithRetry(otlpmetricgrpc.RetryConfig{Enabled: false}),
} }
if !secure { if !secure {
opts = append(opts, otlpmetricgrpc.WithInsecure()) opts = append(opts, otlpmetricgrpc.WithInsecure())
+1 -1
View File
@@ -85,7 +85,7 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
return func(err error) { return func(err error) {
// Use a new context for the export so that the command being cancelled // Use a new context for the export so that the command being cancelled
// doesn't affect the metrics, and we get metrics for cancelled commands. // doesn't affect the metrics, and we get metrics for cancelled commands.
ctx, cancel := context.WithTimeout(context.Background(), ExportTimeout) ctx, cancel := context.WithTimeout(context.Background(), exportTimeout)
defer cancel() defer cancel()
duration := float64(time.Since(start)) / float64(time.Millisecond) duration := float64(time.Since(start)) / float64(time.Millisecond)
+4
View File
@@ -255,6 +255,7 @@ func mergeLoggingConfig(dst, src reflect.Value) error {
return nil return nil
} }
//nolint:unparam
func mergeUlimitsConfig(dst, src reflect.Value) error { func mergeUlimitsConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() { if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
dst.Elem().Set(src.Elem()) dst.Elem().Set(src.Elem())
@@ -262,6 +263,7 @@ func mergeUlimitsConfig(dst, src reflect.Value) error {
return nil return nil
} }
//nolint:unparam
func mergeShellCommand(dst, src reflect.Value) error { func mergeShellCommand(dst, src reflect.Value) error {
if src.Len() != 0 { if src.Len() != 0 {
dst.Set(src) dst.Set(src)
@@ -269,6 +271,7 @@ func mergeShellCommand(dst, src reflect.Value) error {
return nil return nil
} }
//nolint:unparam
func mergeServiceNetworkConfig(dst, src reflect.Value) error { func mergeServiceNetworkConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() { if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
dst.Elem().FieldByName("Aliases").Set(src.Elem().FieldByName("Aliases")) dst.Elem().FieldByName("Aliases").Set(src.Elem().FieldByName("Aliases"))
@@ -282,6 +285,7 @@ func mergeServiceNetworkConfig(dst, src reflect.Value) error {
return nil return nil
} }
//nolint:unparam
func mergeUint64(dst, src reflect.Value) error { func mergeUint64(dst, src reflect.Value) error {
if !src.IsNil() { if !src.IsNil() {
dst.Elem().Set(src.Elem()) dst.Elem().Set(src.Elem())
+10 -10
View File
@@ -5,13 +5,13 @@ go 1.26.0
require ( require (
github.com/containerd/errdefs v1.0.0 github.com/containerd/errdefs v1.0.0
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v29.8.2+incompatible github.com/docker/cli v29.7.2+incompatible
github.com/docker/cli-docs-tool v0.11.0 github.com/docker/cli-docs-tool v0.11.0
github.com/docker/distribution v2.8.3+incompatible github.com/docker/distribution v2.8.3+incompatible
github.com/docker/go-connections v0.8.2 github.com/docker/go-connections v0.8.1
github.com/fvbommel/sortorder v1.2.0 github.com/fvbommel/sortorder v1.2.0
github.com/moby/moby/api v1.56.1 github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.6.2 github.com/moby/moby/client v0.5.1
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/sirupsen/logrus v1.10.2 github.com/sirupsen/logrus v1.10.2
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
@@ -22,7 +22,7 @@ require (
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
@@ -66,11 +66,11 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect
go.opentelemetry.io/otel/trace v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/crypto v0.57.0 // indirect golang.org/x/crypto v0.56.0 // indirect
golang.org/x/net v0.60.0 // indirect golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.48.0 // indirect golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.46.0 // indirect golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.42.0 // indirect golang.org/x/text v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/grpc v1.83.2 // indirect google.golang.org/grpc v1.83.2 // indirect
+21 -20
View File
@@ -1,5 +1,5 @@
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -42,8 +42,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ= github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE= github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q= github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -54,8 +54,8 @@ github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY= github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw= github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw=
@@ -133,10 +133,10 @@ github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KH
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c= github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4= github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc= github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
@@ -236,26 +236,27 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+3 -6
View File
@@ -27,13 +27,8 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
})) }))
cli.SetOut(outStream) cli.SetOut(outStream)
r, w, err := os.Pipe() r, _, err := os.Pipe()
assert.NilError(t, err) assert.NilError(t, err)
t.Cleanup(func() {
_ = w.Close()
_ = r.Close()
})
cli.SetIn(streams.NewIn(r)) cli.SetIn(streams.NewIn(r))
notifyCtx, notifyCancel := context.WithCancel(ctx) notifyCtx, notifyCancel := context.WithCancel(ctx)
@@ -63,6 +58,8 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
}() }()
} }
assert.Check(t, cli.OutBuffer().Len() > 0)
// a small delay to ensure the plugin is prompting // a small delay to ensure the plugin is prompting
time.Sleep(100 * time.Microsecond) time.Sleep(100 * time.Microsecond)
+4 -36
View File
@@ -11,9 +11,8 @@ import (
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/moby/moby/api/types/jsonstream" "github.com/docker/cli/internal/jsonstream"
registrytypes "github.com/moby/moby/api/types/registry" registrytypes "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client/pkg/jsonmessage"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/data"
@@ -47,7 +46,7 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *Repo
// Count the times of calling for handleTarget, // Count the times of calling for handleTarget,
// if it is called more that once, that should be considered an error in a trusted push. // if it is called more that once, that should be considered an error in a trusted push.
cnt := 0 cnt := 0
handleTarget := func(msg jsonstream.Message) { handleTarget := func(msg jsonstream.JSONMessage) {
cnt++ cnt++
if cnt > 1 { if cnt > 1 {
// handleTarget should only be called once. This will be treated as an error. // handleTarget should only be called once. This will be treated as an error.
@@ -79,14 +78,14 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *Repo
default: default:
// We want trust signatures to always take an explicit tag, // We want trust signatures to always take an explicit tag,
// otherwise it will act as an untrusted push. // otherwise it will act as an untrusted push.
if err := displayJSONMessages(ctx, in, ioStreams.Out(), nil); err != nil { if err := jsonstream.Display(ctx, in, ioStreams.Out()); err != nil {
return err return err
} }
_, _ = fmt.Fprintln(ioStreams.Err(), "No tag specified, skipping trust metadata push") _, _ = fmt.Fprintln(ioStreams.Err(), "No tag specified, skipping trust metadata push")
return nil return nil
} }
if err := displayJSONMessages(ctx, in, ioStreams.Out(), handleTarget); err != nil { if err := jsonstream.Display(ctx, in, ioStreams.Out(), jsonstream.WithAuxCallback(handleTarget)); err != nil {
return err return err
} }
@@ -148,34 +147,3 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *Repo
_, _ = fmt.Fprintf(ioStreams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag) _, _ = fmt.Fprintf(ioStreams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag)
return nil return nil
} }
type ctxReader struct {
err chan error
r io.Reader
}
func (r *ctxReader) Read(p []byte) (int, error) {
select {
case err := <-r.err:
return 0, err
default:
return r.r.Read(p)
}
}
// displayJSONMessages prints the JSON messages from in to out. It returns
// ctx.Err() if ctx is canceled, but not when in is closed.
func displayJSONMessages(ctx context.Context, in io.Reader, out *streams.Out, auxCallback func(jsonstream.Message)) error {
if ctx.Err() != nil {
return ctx.Err()
}
reader := &ctxReader{err: make(chan error, 1), r: in}
stop := context.AfterFunc(ctx, func() { reader.err <- ctx.Err() })
defer stop()
if err := jsonmessage.DisplayJSONMessagesStream(reader, out, out.FD(), out.IsTerminal(), auxCallback); err != nil {
return err
}
return ctx.Err()
}
+32
View File
@@ -0,0 +1,32 @@
package trust
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(newTrustCommand)
}
// newTrustCommand returns a cobra command for `trust` subcommands.
func newTrustCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "trust",
Short: "Manage trust on Docker images",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()),
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
newRevokeCommand(dockerCLI),
newSignCommand(dockerCLI),
newTrustKeyCommand(dockerCLI),
newTrustSignerCommand(dockerCLI),
newInspectCommand(dockerCLI),
)
return cmd
}
+2 -2
View File
@@ -5,12 +5,12 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cmd/docker-trust/internal/trust" "github.com/docker/cli/cmd/docker-trust/internal/trust"
"github.com/docker/cli/internal/lazyregexp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/theupdateframework/notary" "github.com/theupdateframework/notary"
"github.com/theupdateframework/notary/trustmanager" "github.com/theupdateframework/notary/trustmanager"
@@ -41,7 +41,7 @@ func newKeyGenerateCommand(dockerCLI command.Streams) *cobra.Command {
} }
// key names can use lowercase alphanumeric + _ + - characters // key names can use lowercase alphanumeric + _ + - characters
var validKeyName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString var validKeyName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
// validate that all of the key names are unique and are alphanumeric + _ + - // validate that all of the key names are unique and are alphanumeric + _ + -
// and that we do not already have public key files in the target dir on disk // and that we do not already have public key files in the target dir on disk
+2 -2
View File
@@ -7,12 +7,12 @@ import (
"io" "io"
"os" "os"
"path" "path"
"regexp"
"strings" "strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cmd/docker-trust/internal/trust" "github.com/docker/cli/cmd/docker-trust/internal/trust"
"github.com/docker/cli/internal/lazyregexp"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/client"
@@ -45,7 +45,7 @@ func newSignerAddCommand(dockerCLI command.Cli) *cobra.Command {
return cmd return cmd
} }
var validSignerName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString var validSignerName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
func addSigner(ctx context.Context, dockerCLI command.Cli, options signerAddOptions) error { func addSigner(ctx context.Context, dockerCLI command.Cli, options signerAddOptions) error {
signerName := options.signer signerName := options.signer
-322
View File
@@ -1,322 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli"
pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
contextdocker "github.com/docker/cli/cli/context/docker"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// processCloud resolves the global --cloud[=NAME] option after configuration is
// loaded, but before telemetry and endpoint initialization.
// The option appears in help and completion only when the selected provider is
// installed, valid, and declares support for the resolver contract.
// The provider defaults to the offload plugin; features.cloud in the effective
// Docker config.json can override the plugin name, not its executable path.
// Explicit use also requires the provider's capability declaration.
// Discovery for visibility does not provision a context or connect to a daemon.
//
// # Provider contract
//
// The provider must include "Features": {"cloud-context-resolver": true} in its
// docker-cli-plugin-metadata response to declare support for this contract.
// Older plugins without this declaration leave the flag hidden and cannot be
// used for resolution.
// The CLI uses normal plugin discovery and invokes:
//
// docker-<provider> --config=<dir> <provider> __resolve-context -- <name>
//
// Bare --cloud and --cloud= pass default as the target name.
// The effective config directory is also passed as DOCKER_CONFIG.
// The provider must provision into that context store without changing the saved
// current context or recursively forwarding --cloud.
// It inherits the environment and sends progress and prompts to stderr. Stdin
// is forwarded only when both stdin and stderr are terminals with file handles;
// wrapped Windows consoles use the corresponding standard handles. Otherwise
// the provider receives EOF and must not prompt or open a terminal
// separately. Piped and redirected input belongs to the requested command.
// Stdout must contain exactly one JSON object:
//
// {"DOCKER_CONTEXT":"provisioned-context"}
//
// The returned context must exist, must not be the virtual default context, and
// must have a Docker endpoint with a nonempty host.
// Resolver failures abort command execution without falling back to another
// context.
// Cancellation terminates the resolver process; provisioning subprocesses and
// resource cleanup remain the provider's responsibility.
// Downstream plugins receive --context=<resolved-name> instead of the global
// --cloud option.
// The provider implementation ships separately and must support this operation
// before enabling the integration.
func processCloud(ctx context.Context, dockerCli *command.DockerCli, cmd *cobra.Command, args, osArgs []string) ([]string, error) {
if !cmd.Flags().Changed("cloud") {
return osArgs, nil
}
if cmd.Flags().Changed("context") || cmd.Flags().Changed("host") {
return osArgs, errors.New("conflicting options: cannot specify --cloud together with --context or --host")
}
help, err := cloudHelpRequest(cmd, args)
if err != nil {
return osArgs, err
}
var contextName string
if !help {
name, _ := cmd.Flags().GetString("cloud")
if name == "" {
name = "default"
}
contextName, err = resolveCloudContext(ctx, dockerCli, cmd, name)
if err != nil {
return osArgs, fmt.Errorf("--cloud: %w", err)
}
if err := cmd.Flags().Set("context", contextName); err != nil {
return osArgs, err
}
}
return cloudPluginArgs(cmd, osArgs, contextName)
}
// cloudResolverFeature is the plugin metadata feature that declares support for
// the __resolve-context contract.
const cloudResolverFeature = "cloud-context-resolver"
const cloudPluginHelp = "to use --cloud, install the plugin. See https://docs.docker.com/go/cloud-flag/"
func cloudProvider(dockerCli config.Provider, rootCmd *cobra.Command) (*pluginmanager.Plugin, error) {
provider := dockerCli.ConfigFile().Features["cloud"]
if provider == "" {
provider = "offload"
}
var help string
if provider == "offload" {
help = "\n\n" + cloudPluginHelp
}
plugin, err := pluginmanager.GetPlugin(provider, dockerCli, rootCmd)
if err != nil {
if provider == "offload" && errdefs.IsNotFound(err) {
return nil, errors.New(cloudPluginHelp)
}
return nil, fmt.Errorf("plugin %q unavailable: %w%s", provider, err, help)
}
if plugin.Err != nil {
return nil, fmt.Errorf("invalid plugin %q: %w%s", provider, plugin.Err, help)
}
if supported, _ := plugin.Features[cloudResolverFeature].(bool); !supported {
return nil, fmt.Errorf("plugin %q does not support --cloud%s", provider, help)
}
return plugin, nil
}
// updateCloudFlagVisibility runs only for help and completion, avoiding plugin
// discovery on ordinary invocations that do not use --cloud.
func updateCloudFlagVisibility(dockerCli config.Provider, rootCmd *cobra.Command) {
flag := rootCmd.Flags().Lookup("cloud")
if flag == nil {
return
}
_, err := cloudProvider(dockerCli, rootCmd)
flag.Hidden = err != nil
}
func resolveCloudContext(ctx context.Context, dockerCli *command.DockerCli, rootCmd *cobra.Command, name string) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}
plugin, err := cloudProvider(dockerCli, rootCmd)
if err != nil {
return "", err
}
cmd := exec.CommandContext(ctx, plugin.Path, "--config="+config.Dir(), plugin.Name, "__resolve-context", "--", name) // #nosec G204 -- executable validated through CLI plugin discovery
cmd.Env = append(os.Environ(), config.EnvOverrideConfigDir+"="+config.Dir(), metadata.ReexecEnvvar+"="+os.Args[0])
setResolverStdio(cmd, dockerCli)
out, err := cmd.Output()
if err != nil {
if ctx.Err() != nil {
return "", ctx.Err()
}
return "", fmt.Errorf("cloud resolver failed: %w", err)
}
var response struct {
DockerContext string `json:"DOCKER_CONTEXT"`
}
if err := json.Unmarshal(out, &response); err != nil {
return "", fmt.Errorf("invalid cloud resolver response: %w", err)
}
response.DockerContext = strings.TrimSpace(response.DockerContext)
if response.DockerContext == "" || response.DockerContext == command.DefaultContextName {
return "", errors.New("cloud resolver must return a non-default DOCKER_CONTEXT")
}
if err := validateResolvedContext(dockerCli, response.DockerContext); err != nil {
return "", err
}
return response.DockerContext, nil
}
// setResolverStdio connects the resolver to the terminal when both stdin and
// stderr are terminals, so the plugin can prompt interactively. Otherwise
// stderr is forwarded and stdin is left unset.
func setResolverStdio(cmd *exec.Cmd, dockerCli *command.DockerCli) {
cmd.Stderr = dockerCli.Err()
stdinTerminal := dockerCli.In().IsTerminal()
stderrTerminal := dockerCli.Err().IsTerminal()
stdin, stdinFile := dockerCli.In().File()
stderr, stderrFile := dockerCli.Err().File()
if runtime.GOOS == "windows" {
// term.StdStreams can wrap console handles for terminal emulation,
// preventing File from exposing them to the child process.
if stdinTerminal && !stdinFile {
stdin, stdinFile = os.Stdin, true
}
if stderrTerminal && !stderrFile {
stderr, stderrFile = os.Stderr, true
}
}
if stdinTerminal && stderrTerminal && stdinFile && stderrFile {
// Pass files directly: wrapping them makes os/exec copy through pipes,
// hiding terminal identity and potentially consuming the command's input.
cmd.Stdin = stdin
cmd.Stderr = stderr
}
}
// validateResolvedContext checks that the context exists and has a Docker
// endpoint host, so a missing context or endpoint cannot fall back to the
// local engine.
func validateResolvedContext(dockerCli *command.DockerCli, name string) error {
meta, err := dockerCli.ContextStore().GetMetadata(name)
if err != nil {
return fmt.Errorf("loading resolved context %q: %w", name, err)
}
endpoint, err := contextdocker.EndpointFromContext(meta)
if err != nil {
return fmt.Errorf("invalid resolved context %q: %w", name, err)
}
if endpoint.Host == "" {
return fmt.Errorf("resolved context %q has no Docker endpoint host", name)
}
return nil
}
// cloudHelpRequest avoids provisioning for help and shell completion.
// Parse known command flags rather than scanning argv: --help may be an option
// value or an argument to a container, not a request for Docker help.
// Unknown flags and missing flag values return a usage error, so a mistyped
// invocation fails without provisioning or falling back to the local engine.
// Flag values are validated only by the command's own parse, after resolution.
func cloudHelpRequest(rootCmd *cobra.Command, args []string) (bool, error) {
help, _ := rootCmd.PersistentFlags().GetBool("help")
version, _ := rootCmd.Flags().GetBool("version")
if help || version || len(args) == 0 {
return true, nil
}
switch args[0] {
case "help", "completion", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
return true, nil
}
cmd, remaining, err := rootCmd.Find(args)
if err != nil || cmd == rootCmd || pluginmanager.IsPluginCommand(cmd) {
// Plugin flags are opaque. Only recognize help immediately after the
// plugin name; deeper help is available through "docker help PLUGIN".
return len(args) > 1 && isHelpFlag(args[1]), nil
}
// "build", "bake", "builder", and "image build" are builtin commands that
// processAliases may still rewrite into a call to the builder plugin
// (buildx) after cloud resolution runs, so their flag set here is only the
// legacy builder's and does not reflect what will actually parse the
// remaining args. Treat them like a plugin command above: only recognize
// help immediately after the resolved command path.
if _, _, _, forwarded := forwardBuilder(builderDefaultPlugin, args, args); forwarded {
return len(remaining) > 0 && isHelpFlag(remaining[0]), nil
}
cmd.InitDefaultHelpFlag()
flags := cmd.Flags()
flags.AddFlagSet(cmd.PersistentFlags())
flags.AddFlagSet(cmd.InheritedFlags())
err = flags.ParseAll(remaining, func(flag *pflag.Flag, value string) error {
if flag.Name == "help" {
var err error
if help, err = strconv.ParseBool(value); err != nil {
return fmt.Errorf("invalid argument %q for \"--help\" flag: %w", value, err)
}
}
return nil
})
if err != nil {
// Format the usage error directly. The root command's FlagErrorFunc
// first checks whether the daemon supports the command, which would
// connect to the engine selected before --cloud is resolved.
return false, cli.FlagErrorFunc(cmd, err)
}
return help, nil
}
// isHelpFlag reports whether arg requests help, in any of the forms pflag
// accepts for a boolean flag.
func isHelpFlag(arg string) bool {
return arg == "--help" || arg == "-h" || arg == "--help=true"
}
// cloudPluginArgs only rewrites the global prefix, leaving subcommand arguments
// untouched. Parsing also distinguishes --cloud from another flag's value.
func cloudPluginArgs(cmd *cobra.Command, osArgs []string, contextName string) ([]string, error) {
flags := pflag.NewFlagSet(cmd.Name(), pflag.ContinueOnError)
flags.SetInterspersed(false)
flags.AddFlagSet(cmd.Flags())
flags.AddFlagSet(cmd.PersistentFlags())
result := []string{osArgs[0]}
if contextName != "" {
result = append(result, "--context="+contextName)
}
if err := flags.ParseAll(osArgs[1:], func(flag *pflag.Flag, value string) error {
if flag.Name != "cloud" {
result = append(result, "--"+flag.Name+"="+value)
}
return nil
}); err != nil {
return osArgs, err
}
if flags.ArgsLenAtDash() >= 0 {
result = append(result, "--")
}
return append(result, flags.Args()...), nil
}
-682
View File
@@ -1,682 +0,0 @@
package main
import (
"bytes"
"cmp"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/creack/pty"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
contextdocker "github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestCloudFlagHidden(t *testing.T) {
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs([]string{"--cloud=team", "--help"})
cmd, _, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
name, err := cmd.Flags().GetString("cloud")
assert.NilError(t, err)
assert.Equal(t, name, "team")
assert.Assert(t, cmd.Flags().Lookup("cloud").Hidden)
assert.Assert(t, !strings.Contains(cmd.UsageString(), "--cloud"))
}
func TestCloudHelpRequest(t *testing.T) {
for _, tc := range []struct {
name string
args []string
help bool
}{
{name: "no command", help: true},
{name: "global help", args: []string{"--help"}, help: true},
{name: "global version", args: []string{"--version"}, help: true},
{name: "help command", args: []string{"help", "run"}, help: true},
{name: "completion request", args: []string{"__complete", "run", ""}, help: true},
{name: "completion script", args: []string{"completion", "fish"}, help: true},
{name: "builtin help", args: []string{"run", "--help"}, help: true},
{name: "boolean help value", args: []string{"run", "--help=1"}, help: true},
{name: "nested help", args: []string{"container", "run", "--help"}, help: true},
{name: "plugin help", args: []string{"compose", "--help"}, help: true},
{name: "command execution", args: []string{"ps"}},
{name: "container help", args: []string{"run", "alpine", "--help"}},
{name: "flag value", args: []string{"run", "--name", "--help", "alpine"}},
{name: "help disabled", args: []string{"run", "--help=false", "alpine"}},
{name: "build help", args: []string{"build", "--help"}, help: true},
{name: "build buildkit-only flag", args: []string{"build", "--secret", "id=s,src=./s", "."}},
{name: "image build buildkit-only flag", args: []string{"image", "build", "--push", "."}},
{name: "builder build buildkit-only flag", args: []string{"builder", "build", "--ssh", "default", "."}},
{name: "bake help", args: []string{"bake", "--help"}, help: true},
} {
t.Run(tc.name, func(t *testing.T) {
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs(append([]string{"--cloud"}, tc.args...))
cmd, args, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
help, err := cloudHelpRequest(cmd, args)
assert.NilError(t, err)
assert.Equal(t, help, tc.help)
})
}
}
// Flag errors are reported before cloud resolution, so they must not run the
// command's daemon feature checks against the engine selected before it.
func TestCloudFlagErrorSkipsDaemon(t *testing.T) {
originalConfig := config.Dir()
t.Cleanup(func() { config.SetDir(originalConfig) })
config.SetDir(t.TempDir())
var pings atomic.Int32
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
pings.Add(1)
w.Header().Set("Docker-Experimental", "false")
}))
defer daemon.Close()
t.Setenv("DOCKER_HOST", "tcp://"+daemon.Listener.Addr().String())
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs([]string{"--cloud", "checkpoint", "ls", "--unknown"})
cmd, args, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
assert.NilError(t, tcmd.Initialize())
_, err = cloudHelpRequest(cmd, args)
assert.Check(t, is.ErrorContains(err, "unknown flag: --unknown"))
assert.Check(t, is.Equal(pings.Load(), int32(0)))
}
func TestCloudPluginArgs(t *testing.T) {
for _, tc := range []struct {
name string
args []string
context string
want []string
}{
{
name: "global flag only",
args: []string{"docker", "--cloud=foo", "compose", "up", "--cloud=child"},
context: "cloud-foo",
want: []string{"docker", "--context=cloud-foo", "compose", "up", "--cloud=child"},
},
{
name: "flag value is not cloud flag",
args: []string{"docker", "--config", "--cloud", "--cloud", "-D", "compose", "up"},
context: "cloud-default",
want: []string{"docker", "--context=cloud-default", "--config=--cloud", "--debug=true", "compose", "up"},
},
{
name: "delimiter preserved",
args: []string{"docker", "--cloud=foo", "--", "compose", "--", "--cloud"},
context: "cloud-foo",
want: []string{"docker", "--context=cloud-foo", "--", "compose", "--", "--cloud"},
},
{
name: "repeated cloud flags",
args: []string{"docker", "--cloud=foo", "--cloud=bar", "compose", "up"},
context: "cloud-bar",
want: []string{"docker", "--context=cloud-bar", "compose", "up"},
},
{
name: "help without resolution",
args: []string{"docker", "--cloud", "compose", "--help"},
want: []string{"docker", "compose", "--help"},
},
} {
t.Run(tc.name, func(t *testing.T) {
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs(tc.args[1:])
cmd, _, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
got, err := cloudPluginArgs(cmd, tc.args, tc.context)
assert.NilError(t, err)
assert.DeepEqual(t, got, tc.want)
})
}
}
// Exercise the real startup and subprocess boundary without a daemon or cloud
// service, including failures that must never dispatch the requested command.
func TestCloudResolution(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fixture plugins use shell scripts")
}
executable, err := os.Executable()
assert.NilError(t, err)
for _, tc := range []struct {
name string
args []string
response string
exit int
wantName string
wantErr string
plugin bool
skip bool
provider string
noProvider bool
legacyProvider bool
invalidProvider bool
wantHelp bool
command []string
visible bool
}{
{
name: "default target",
args: []string{"--cloud"},
wantName: "default",
},
{
name: "empty target",
args: []string{"--cloud="},
wantName: "default",
},
{
name: "named target",
args: []string{"--cloud=team"},
wantName: "team",
},
{
name: "plugin dispatch",
args: []string{"--cloud=team"},
wantName: "team",
plugin: true,
},
{
name: "nonzero exit",
args: []string{"--cloud"},
exit: 1,
wantErr: "cloud resolver failed",
},
{
name: "malformed JSON",
args: []string{"--cloud"},
response: "not JSON",
wantErr: "invalid cloud resolver response",
},
{
name: "multiple responses",
args: []string{"--cloud"},
response: `{} {}`,
wantErr: "invalid cloud resolver response",
},
{
name: "missing field",
args: []string{"--cloud"},
response: `{}`,
wantErr: "must return a non-default DOCKER_CONTEXT",
},
{
name: "local context",
args: []string{"--cloud"},
response: `{"DOCKER_CONTEXT":"default"}`,
wantErr: "must return a non-default DOCKER_CONTEXT",
},
{
name: "missing context",
args: []string{"--cloud"},
response: `{"DOCKER_CONTEXT":"missing"}`,
wantErr: `loading resolved context "missing"`,
},
{
name: "missing endpoint host",
args: []string{"--cloud"},
response: `{"DOCKER_CONTEXT":"empty-host"}`,
wantErr: "has no Docker endpoint host",
},
{
name: "explicit context conflict",
args: []string{"--cloud", "--context=other"},
wantErr: "conflicting options",
skip: true,
},
{
name: "explicit host conflict",
args: []string{"--cloud", "--host=tcp://localhost:2375"},
wantErr: "conflicting options",
skip: true,
},
{
name: "invalid flag before help",
args: []string{"--cloud"},
command: []string{"run", "--unknown", "--help"},
wantErr: "unknown flag: --unknown",
skip: true,
},
{
name: "default provider",
args: []string{"--cloud"},
noProvider: true,
wantName: "default",
},
{
name: "legacy default provider",
args: []string{"--cloud"},
noProvider: true,
legacyProvider: true,
wantErr: `plugin "offload" does not support --cloud`,
wantHelp: true,
skip: true,
},
{
name: "invalid default provider",
args: []string{"--cloud"},
noProvider: true,
invalidProvider: true,
wantErr: `invalid plugin "offload": plugin metadata does not define a vendor`,
wantHelp: true,
skip: true,
},
{
name: "invalid configured provider",
args: []string{"--cloud"},
invalidProvider: true,
wantErr: `invalid plugin "foobar": plugin metadata does not define a vendor`,
skip: true,
},
{
name: "legacy configured provider",
args: []string{"--cloud"},
legacyProvider: true,
wantErr: `plugin "foobar" does not support --cloud`,
skip: true,
},
{
name: "configured provider unavailable",
args: []string{"--cloud"},
provider: "missing",
wantErr: `plugin "missing" unavailable`,
skip: true,
},
{
name: "provider cannot be a path",
args: []string{"--cloud"},
provider: "../foobar",
wantErr: `plugin "../foobar" unavailable`,
skip: true,
},
{
name: "no cloud option",
noProvider: true,
skip: true,
},
{
name: "help",
args: []string{"--cloud", "--help"},
noProvider: true,
skip: true,
},
{
name: "version",
args: []string{"--cloud", "--version"},
noProvider: true,
skip: true,
},
{
name: "help exposes supported provider",
command: []string{"--help"},
noProvider: true,
visible: true,
skip: true,
},
{
name: "help hides legacy provider",
command: []string{"--help"},
noProvider: true,
legacyProvider: true,
skip: true,
},
{
name: "help hides missing provider",
command: []string{"--help"},
provider: "missing",
skip: true,
},
{
name: "completion exposes supported provider",
command: []string{"__complete", "--cl"},
noProvider: true,
visible: true,
skip: true,
},
{
name: "completion hides legacy provider",
command: []string{"__complete", "--cl"},
noProvider: true,
legacyProvider: true,
skip: true,
},
{
name: "completion hides missing provider",
command: []string{"__complete", "--cl"},
provider: "missing",
skip: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
// Explicit --cloud must override inherited endpoint selection.
t.Setenv("DOCKER_CONTEXT", "inherited")
t.Setenv("DOCKER_HOST", "tcp://127.0.0.1:1")
t.Setenv("DOCKER_CLI_HOOKS", "false")
t.Setenv("DOCKER_CONFIG", t.TempDir())
configDir := t.TempDir()
installedProvider := "offload"
if !tc.noProvider {
installedProvider = "foobar"
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
provider := cmp.Or(tc.provider, "foobar")
cfg.Features = map[string]string{"cloud": provider}
assert.NilError(t, cfg.Save())
}
pluginDir := filepath.Join(configDir, "cli-plugins")
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
response := cmp.Or(tc.response, `{"DOCKER_CONTEXT":"resolved"}`)
providerMetadata := metadata.Metadata{SchemaVersion: "0.1.0", Vendor: "test"}
if tc.invalidProvider {
providerMetadata.Vendor = ""
}
if !tc.legacyProvider {
providerMetadata.Features = map[string]any{cloudResolverFeature: true}
}
pluginMetadata, err := json.Marshal(providerMetadata)
assert.NilError(t, err)
script := fmt.Sprintf(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '%s'
exit 0
fi
printf '%%s\n' "$@" >> "$DOCKER_CONFIG/resolver-args"
echo provisioning >&2
printf '%%s\n' '%s'
exit %d
`, pluginMetadata, response, tc.exit)
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+installedProvider), []byte(script), 0o755))
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-cloudtest"), []byte(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"test"}'
exit 0
fi
printf '%s\n' "$@" > "$CLOUD_TEST_ARGS"
`), 0o755))
dispatchFile := filepath.Join(configDir, "dispatch-args")
t.Setenv("CLOUD_TEST_ARGS", dispatchFile)
contextStore := store.New(filepath.Join(configDir, "contexts"), command.DefaultContextStoreConfig())
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
Name: "resolved",
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{Host: "tcp://127.0.0.1:1"}},
}))
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
Name: "empty-host",
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{}},
}))
args := append([]string{"docker", "--config=" + configDir}, tc.args...)
switch {
case tc.command != nil:
args = append(args, tc.command...)
case tc.plugin || tc.wantErr != "":
args = append(args, "cloudtest", "up", "--cloud=child")
default:
args = append(args, "context", "show")
}
payload, err := json.Marshal(args)
assert.NilError(t, err)
var stdout, stderr bytes.Buffer
cmd := exec.CommandContext(t.Context(), executable, "-test.run=^TestCloudCommandProcess$")
cmd.Env = append(os.Environ(), "CLOUD_TEST_COMMAND="+string(payload))
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if tc.wantErr != "" {
assert.Assert(t, err != nil)
assert.Check(t, is.Contains(stderr.String(), tc.wantErr))
assert.Equal(t, strings.Contains(stderr.String(), cloudPluginHelp), tc.wantHelp, stderr.String())
_, statErr := os.Stat(dispatchFile)
assert.Assert(t, os.IsNotExist(statErr))
assert.Equal(t, stdout.String(), "")
} else {
assert.NilError(t, err, stderr.String())
if tc.command != nil {
assert.Equal(t, strings.Contains(stdout.String(), "--cloud"), tc.visible, stdout.String())
}
if !tc.skip {
if tc.plugin {
args, err := os.ReadFile(dispatchFile)
assert.NilError(t, err)
assert.Equal(t, string(args), "--context=resolved\n--config="+configDir+"\ncloudtest\nup\n--cloud=child\n")
} else {
assert.Equal(t, stdout.String(), "resolved\n")
}
}
}
invocation, err := os.ReadFile(filepath.Join(configDir, "resolver-args"))
if tc.skip {
assert.Assert(t, os.IsNotExist(err))
} else {
assert.NilError(t, err)
assert.Check(t, is.Contains(stderr.String(), "provisioning"))
name := cmp.Or(tc.wantName, "default")
assert.Equal(t, string(invocation), "--config="+configDir+"\n"+installedProvider+"\n__resolve-context\n--\n"+name+"\n")
}
})
}
}
func TestCloudResolverInput(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fixture plugins use shell scripts and pseudo-terminals")
}
executable, err := os.Executable()
assert.NilError(t, err)
for _, tc := range []struct {
name string
stdinType string
terminalErr bool
prompt bool
}{
{name: "interactive", stdinType: "terminal", terminalErr: true, prompt: true},
{name: "piped stdin", stdinType: "pipe", terminalErr: true},
{name: "redirected stdin", stdinType: "file", terminalErr: true},
{name: "redirected stderr", stdinType: "terminal"},
{name: "noninteractive", stdinType: "pipe"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("DOCKER_CLI_HOOKS", "false")
configDir := t.TempDir()
pluginDir := filepath.Join(configDir, "cli-plugins")
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-offload"), []byte(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"test","Features":{"cloud-context-resolver":true}}'
exit 0
fi
if [ -t 0 ]; then
[ -t 2 ] || exit 1
printf 'Continue? ' >&2
IFS= read -r reply || exit 1
printf '%s\n' "$reply" > "$DOCKER_CONFIG/resolver-input"
elif IFS= read -r unexpected; then
echo 'resolver consumed command input' >&2
exit 1
fi
echo '{"DOCKER_CONTEXT":"resolved"}'
`), 0o755))
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-cloudtest"), []byte(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"test"}'
exit 0
fi
IFS= read -r input || exit 1
printf '%s\n' "$input"
`), 0o755))
contextStore := store.New(filepath.Join(configDir, "contexts"), command.DefaultContextStoreConfig())
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
Name: "resolved",
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{Host: "tcp://127.0.0.1:1"}},
}))
terminal, tty, err := pty.Open()
assert.NilError(t, err)
t.Cleanup(func() {
_ = tty.Close()
_ = terminal.Close()
})
const commandInput = "input for the original command\n"
var stdin *os.File
switch tc.stdinType {
case "terminal":
stdin = tty
input := commandInput
if tc.prompt {
input = "yes\n" + input
}
_, err = terminal.WriteString(input)
assert.NilError(t, err)
case "pipe":
var writer *os.File
stdin, writer, err = os.Pipe()
assert.NilError(t, err)
t.Cleanup(func() { _ = stdin.Close() })
t.Cleanup(func() { _ = writer.Close() })
_, err = writer.WriteString(commandInput)
assert.NilError(t, err)
assert.NilError(t, writer.Close())
case "file":
inputPath := filepath.Join(configDir, "command-input")
assert.NilError(t, os.WriteFile(inputPath, []byte(commandInput), 0o600))
stdin, err = os.Open(inputPath)
assert.NilError(t, err)
t.Cleanup(func() { _ = stdin.Close() })
}
payload, err := json.Marshal([]string{"docker", "--config=" + configDir, "--cloud", "cloudtest"})
assert.NilError(t, err)
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
var stdout, stderr bytes.Buffer
cmd := exec.CommandContext(ctx, executable, "-test.run=^TestCloudCommandProcess$")
cmd.Env = append(os.Environ(), "CLOUD_TEST_COMMAND="+string(payload))
cmd.Stdin = stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.WaitDelay = time.Second
if tc.terminalErr {
cmd.Stderr = tty
}
assert.NilError(t, cmd.Run(), stderr.String())
assert.Equal(t, stdout.String(), commandInput)
reply, err := os.ReadFile(filepath.Join(configDir, "resolver-input"))
if tc.prompt {
assert.NilError(t, err)
assert.Equal(t, string(reply), "yes\n")
} else {
assert.Assert(t, os.IsNotExist(err))
}
})
}
}
// Run startup in a separate process because it installs process-wide signal
// handlers that intentionally outlive an individual command.
func TestCloudCommandProcess(t *testing.T) {
payload := os.Getenv("CLOUD_TEST_COMMAND")
if payload == "" {
return
}
assert.NilError(t, json.Unmarshal([]byte(payload), &os.Args))
err := dockerMain(context.Background())
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
}
if err == nil {
os.Exit(0)
}
os.Exit(getExitCode(err))
}
func TestCloudResolutionCancellation(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fixture plugin uses a shell script")
}
originalConfig := config.Dir()
t.Cleanup(func() { config.SetDir(originalConfig) })
config.SetDir(t.TempDir())
cfg := configfile.New(filepath.Join(config.Dir(), config.ConfigFileName))
cfg.Features = map[string]string{"cloud": "foobar"}
assert.NilError(t, cfg.Save())
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-foobar"), []byte(`#!/bin/sh
if [ "$1" = docker-cli-plugin-metadata ]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"test","Features":{"cloud-context-resolver":true}}'
exit 0
fi
exec sleep 30
`), 0o755))
dockerCli, err := command.NewDockerCli()
assert.NilError(t, err)
tcmd := newDockerCommand(dockerCli)
tcmd.SetArgs(strings.Fields("--cloud context show"))
cmd, _, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
_, err = resolveCloudContext(ctx, dockerCli, cmd, "default")
assert.ErrorIs(t, err, context.DeadlineExceeded)
}
+4 -33
View File
@@ -178,9 +178,6 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
_ = cmd.RegisterFlagCompletionFunc("log-level", completeLogLevels) _ = cmd.RegisterFlagCompletionFunc("log-level", completeLogLevels)
cmd.Flags().BoolP("version", "v", false, "Print version information and quit") cmd.Flags().BoolP("version", "v", false, "Print version information and quit")
cmd.Flags().String("cloud", "", "Use a cloud context, provisioning it if necessary (optionally --cloud=NAME)")
cmd.Flags().Lookup("cloud").NoOptDefVal = "default"
cmd.Flags().Lookup("cloud").Hidden = true
setFlagErrorFunc(dockerCli, cmd) setFlagErrorFunc(dockerCli, cmd)
setupHelpCommand(dockerCli, cmd, helpCmd) setupHelpCommand(dockerCli, cmd, helpCmd)
@@ -253,7 +250,6 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) {
ccmd.Println(err) ccmd.Println(err)
return return
} }
updateCloudFlagVisibility(dockerCli, ccmd.Root())
if len(args) >= 1 { if len(args) >= 1 {
err := tryRunPluginHelp(dockerCli, ccmd, args) err := tryRunPluginHelp(dockerCli, ccmd, args)
@@ -512,18 +508,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
return err return err
} }
if err := tcmd.Initialize( if err := tcmd.Initialize(command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider()); err != nil {
command.WithEnableGlobalMeterProvider(),
command.WithEnableGlobalTracerProvider(),
command.WithContextResolver(func(cli *command.DockerCli) (string, error) {
var err error
os.Args, err = processCloud(ctx, cli, cmd, args, os.Args)
if err != nil {
return "", err
}
return cmd.Flags().GetString("context")
}),
); err != nil {
return err return err
} }
@@ -553,7 +538,6 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd); err != nil { if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd); err != nil {
return err return err
} }
updateCloudFlagVisibility(dockerCli, cmd)
} }
var subCommand *cobra.Command var subCommand *cobra.Command
@@ -571,24 +555,11 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
return nil return nil
} }
if !errdefs.IsNotFound(err) { if !errdefs.IsNotFound(err) {
// For plugin not found we fall through to
// cmd.Execute() which deals with reporting
// "command not found" in a consistent way.
return err return err
} }
if args[0] == "offload" {
// Report this before cmd.ExecuteContext(): plugin-specific flags such as
// --cpus otherwise fail parsing before the root command's RunE.
return errors.New("docker: to use 'docker offload', install the plugin.\nSee https://docs.docker.com/go/cloud-flag/")
}
// Other missing or invalid plugins fall through to cmd.ExecuteContext(),
// which reports the standard unknown-command error.
//
// Cobra handles "--version" before it checks for unknown commands,
// so unset it to make "docker nonexistent --version" fail instead
// of printing the version. A global "--version" flag (as in
// "docker --version nonexistent") was already parsed, and still
// prints the version.
if !cmd.Flags().Changed("version") {
cmd.Version = ""
}
} }
} }
-1
View File
@@ -4156,7 +4156,6 @@ _docker_manifest_annotate() {
amd64 amd64
arm arm
arm64 arm64
loong64
mips64 mips64
mips64le mips64le
ppc64le ppc64le
+1 -1
View File
@@ -2,7 +2,7 @@
# ALPINE_VERSION sets the version of the alpine base image to use. # ALPINE_VERSION sets the version of the alpine base image to use.
# It must be a supported tag in the docker.io/library/alpine image repository. # It must be a supported tag in the docker.io/library/alpine image repository.
ARG ALPINE_VERSION=3.24 ARG ALPINE_VERSION=3.23
FROM alpine:${ALPINE_VERSION} AS gen FROM alpine:${ALPINE_VERSION} AS gen
RUN apk add --no-cache bash git RUN apk add --no-cache bash git
+3 -3
View File
@@ -1,16 +1,16 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.27.4 ARG GO_VERSION=1.26.8
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # 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 # 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. # that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.24 ARG ALPINE_VERSION=3.23
# BUILDX_VERSION sets the version of buildx to install in the dev container. # BUILDX_VERSION sets the version of buildx to install in the dev container.
# It must be a valid tag in the docker.io/docker/buildx-bin image repository # It must be a valid tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub. # on Docker Hub.
ARG BUILDX_VERSION=0.38.2 ARG BUILDX_VERSION=0.34.1
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
+3 -3
View File
@@ -1,13 +1,13 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.27.4 ARG GO_VERSION=1.26.8
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # 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 # 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. # that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.24 ARG ALPINE_VERSION=3.23
# GOLANGCI_LINT_VERSION sets the version of the golangci/golangci-lint image to use. # GOLANGCI_LINT_VERSION sets the version of the golangci/golangci-lint image to use.
ARG GOLANGCI_LINT_VERSION=v2.14.0 ARG GOLANGCI_LINT_VERSION=v2.10.1
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
+3 -3
View File
@@ -1,12 +1,12 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.27.4 ARG GO_VERSION=1.26.8
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # 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 # 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. # that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.24 ARG ALPINE_VERSION=3.23
ARG MODOUTDATED_VERSION=v0.9.0 ARG MODOUTDATED_VERSION=v0.8.0
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
ENV GOTOOLCHAIN=local ENV GOTOOLCHAIN=local
+1364 -1
View File
File diff suppressed because it is too large Load Diff
+160 -2
View File
@@ -1,7 +1,165 @@
--- ---
title: Volume plugin for Amazon EBS
description: Volume plugin for Amazon EBS description: Volume plugin for Amazon EBS
keywords: "API, Usage, plugins, documentation, developer, amazon, ebs, rexray, volume" keywords: "API, Usage, plugins, documentation, developer, amazon, ebs, rexray, volume"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. <!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# Volume plugin for Amazon EBS
## A proof-of-concept Rexray plugin
In this example, a simple Rexray plugin will be created for the purposes of using
it on an Amazon EC2 instance with EBS. It is not meant to be a complete Rexray plugin.
The example source is available at [https://github.com/tiborvass/rexray-plugin](https://github.com/tiborvass/rexray-plugin).
To learn more about Rexray: [https://github.com/codedellemc/rexray](https://github.com/codedellemc/rexray)
## 1. Make a Docker image
The following is the Dockerfile used to containerize rexray.
```dockerfile
FROM debian:jessie
RUN apt-get update && apt-get install -y --no-install-recommends wget ca-certificates
RUN wget https://dl.bintray.com/emccode/rexray/stable/0.6.4/rexray-Linux-x86_64-0.6.4.tar.gz -O rexray.tar.gz && tar -xvzf rexray.tar.gz -C /usr/bin && rm rexray.tar.gz
RUN mkdir -p /run/docker/plugins /var/lib/libstorage/volumes
ENTRYPOINT ["rexray"]
CMD ["--help"]
```
To build it you can run `image=$(cat Dockerfile | docker build -q -)` and `$image`
will reference the containerized rexray image.
## 2. Extract rootfs
```sh
$ TMPDIR=/tmp/rexray # for the purpose of this example
$ # create container without running it, to extract the rootfs from image
$ docker create --name rexray "$image"
$ # save the rootfs to a tar archive
$ docker export -o $TMPDIR/rexray.tar rexray
$ # extract rootfs from tar archive to a rootfs folder
$ ( mkdir -p $TMPDIR/rootfs; cd $TMPDIR/rootfs; tar xf ../rexray.tar )
```
## 3. Add plugin configuration
We have to put the following JSON to `$TMPDIR/config.json`:
```json
{
"Args": {
"Description": "",
"Name": "",
"Settable": null,
"Value": null
},
"Description": "A proof-of-concept EBS plugin (using rexray) for Docker",
"Documentation": "https://github.com/tiborvass/rexray-plugin",
"Entrypoint": [
"/usr/bin/rexray", "service", "start", "-f"
],
"Env": [
{
"Description": "",
"Name": "REXRAY_SERVICE",
"Settable": [
"value"
],
"Value": "ebs"
},
{
"Description": "",
"Name": "EBS_ACCESSKEY",
"Settable": [
"value"
],
"Value": ""
},
{
"Description": "",
"Name": "EBS_SECRETKEY",
"Settable": [
"value"
],
"Value": ""
}
],
"Interface": {
"Socket": "rexray.sock",
"Types": [
"docker.volumedriver/1.0"
]
},
"Linux": {
"AllowAllDevices": true,
"Capabilities": ["CAP_SYS_ADMIN"],
"Devices": null
},
"Mounts": [
{
"Source": "/dev",
"Destination": "/dev",
"Type": "bind",
"Options": ["rbind"]
}
],
"Network": {
"Type": "host"
},
"PropagatedMount": "/var/lib/libstorage/volumes",
"User": {},
"WorkDir": ""
}
```
Note a couple of points:
- `PropagatedMount` is needed so that the docker daemon can see mounts done by the
rexray plugin from within the container, otherwise the docker daemon is not able
to mount a docker volume.
- The rexray plugin needs dynamic access to host devices. For that reason, we
have to give it access to all devices under `/dev` and set `AllowAllDevices` to
true for proper access.
- The user of this simple plugin can change only 3 settings: `REXRAY_SERVICE`,
`EBS_ACCESSKEY` and `EBS_SECRETKEY`. This is because of the reduced scope of this
plugin. Ideally other rexray parameters could also be set.
## 4. Create plugin
`docker plugin create tiborvass/rexray-plugin "$TMPDIR"` will create the plugin.
```sh
$ docker plugin ls
ID NAME DESCRIPTION ENABLED
2475a4bd0ca5 tiborvass/rexray-plugin:latest A rexray volume plugin for Docker false
```
## 5. Test plugin
```sh
$ docker plugin set tiborvass/rexray-plugin EBS_ACCESSKEY=$AWS_ACCESSKEY EBS_SECRETKEY=$AWS_SECRETKEY`
$ docker plugin enable tiborvass/rexray-plugin
$ docker volume create -d tiborvass/rexray-plugin my-ebs-volume
$ docker volume ls
DRIVER VOLUME NAME
tiborvass/rexray-plugin:latest my-ebs-volume
$ docker run --rm -v my-ebs-volume:/volume busybox sh -c 'echo bye > /volume/hi'
$ docker run --rm -v my-ebs-volume:/volume busybox cat /volume/hi
bye
```
## 6. Push plugin
First, ensure you are logged in with `docker login`. Then you can run:
`docker plugin push tiborvass/rexray-plugin` to push it like a regular docker
image to a registry, to make it available for others to install via
`docker plugin install tiborvass/rexray-plugin EBS_ACCESSKEY=$AWS_ACCESSKEY EBS_SECRETKEY=$AWS_SECRETKEY`.
-3
View File
@@ -1,3 +0,0 @@
# Docker Engine managed plugin system
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
+262 -1
View File
@@ -7,4 +7,265 @@ aliases:
- "/engine/extend/plugins_graphdriver/" - "/engine/extend/plugins_graphdriver/"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. - [Installing and using a plugin](index.md#installing-and-using-a-plugin)
- [Developing a plugin](index.md#developing-a-plugin)
- [Debugging plugins](index.md#debugging-plugins)
Docker Engine's plugin system lets you install, start, stop, and remove
plugins using Docker Engine.
For information about legacy (non-managed) plugins, refer to
[Understand legacy Docker Engine plugins](legacy_plugins.md).
> [!NOTE]
> Docker Engine managed plugins are currently not supported on Windows daemons.
## Installing and using a plugin
Plugins are distributed as Docker images and can be hosted on Docker Hub or on
a private registry.
To install a plugin, use the `docker plugin install` command, which pulls the
plugin from Docker Hub or your private registry, prompts you to grant
permissions or capabilities if necessary, and enables the plugin.
To check the status of installed plugins, use the `docker plugin ls` command.
Plugins that start successfully are listed as enabled in the output.
After a plugin is installed, you can use it as an option for another Docker
operation, such as creating a volume.
In the following example, you install the [`rclone` plugin](https://rclone.org/docker/), verify that it is
enabled, and use it to create a volume.
> [!NOTE]
> This example is intended for instructional purposes only.
1. Set up the pre-requisite directories. By default they must exist on the host at the following locations:
- `/var/lib/docker-plugins/rclone/config`. Reserved for the `rclone.conf` config file and must exist even if it's empty and the config file is not present.
- `/var/lib/docker-plugins/rclone/cache`. Holds the plugin state file as well as optional VFS caches.
2. Install the `rclone` plugin.
```console
$ docker plugin install rclone/docker-volume-rclone --alias rclone
Plugin "rclone/docker-volume-rclone" is requesting the following privileges:
- network: [host]
- mount: [/var/lib/docker-plugins/rclone/config]
- mount: [/var/lib/docker-plugins/rclone/cache]
- device: [/dev/fuse]
- capabilities: [CAP_SYS_ADMIN]
Do you grant the above permissions? [y/N]
```
The plugin requests 5 privileges:
- It needs access to the `host` network.
- Access to pre-requisite directories to mount to store:
- Your Rclone config files
- Temporary cache data
- Gives access to the FUSE (Filesystem in Userspace) device. This is required because Rclone uses FUSE to mount remote storage as if it were a local filesystem.
- It needs the `CAP_SYS_ADMIN` capability, which allows the plugin to run
the `mount` command.
2. Check that the plugin is enabled in the output of `docker plugin ls`.
```console
$ docker plugin ls
ID NAME DESCRIPTION ENABLED
aede66158353 rclone:latest Rclone volume plugin for Docker true
```
3. Create a volume using the plugin.
This example mounts the `/remote` directory on host `1.2.3.4` into a
volume named `rclonevolume`.
This volume can now be mounted into containers.
```console
$ docker volume create \
-d rclone \
--name rclonevolume \
-o type=sftp \
-o path=remote \
-o sftp-host=1.2.3.4 \
-o sftp-user=user \
-o "sftp-password=$(cat file_containing_password_for_remote_host)"
```
4. Verify that the volume was created successfully.
```console
$ docker volume ls
DRIVER NAME
rclone rclonevolume
```
5. Start a container that uses the volume `rclonevolume`.
```console
$ docker run --rm -v rclonevolume:/data busybox ls /data
<content of /remote on machine 1.2.3.4>
```
6. Remove the volume `rclonevolume`
```console
$ docker volume rm rclonevolume
sshvolume
```
To disable a plugin, use the `docker plugin disable` command. To completely
remove it, use the `docker plugin remove` command. For other available
commands and options, see the
[command line reference](https://docs.docker.com/reference/cli/docker/).
## Developing a plugin
#### The rootfs directory
The `rootfs` directory represents the root filesystem of the plugin. In this
example, it was created from a Dockerfile:
> [!NOTE]
> The `/run/docker/plugins` directory is mandatory inside of the
> plugin's filesystem for Docker to communicate with the plugin.
```console
$ git clone https://github.com/vieux/docker-volume-sshfs
$ cd docker-volume-sshfs
$ docker build -t rootfsimage .
$ id=$(docker create rootfsimage true) # id was cd851ce43a403 when the image was created
$ sudo mkdir -p myplugin/rootfs
$ sudo docker export "$id" | sudo tar -x -C myplugin/rootfs
$ docker rm -vf "$id"
$ docker rmi rootfsimage
```
#### The config.json file
The `config.json` file describes the plugin. See the [plugins config reference](config.md).
Consider the following `config.json` file.
```json
{
"description": "sshFS plugin for Docker",
"documentation": "https://docs.docker.com/engine/extend/plugins/",
"entrypoint": ["/docker-volume-sshfs"],
"network": {
"type": "host"
},
"interface": {
"types": ["docker.volumedriver/1.0"],
"socket": "sshfs.sock"
},
"linux": {
"capabilities": ["CAP_SYS_ADMIN"]
}
}
```
This plugin is a volume driver. It requires a `host` network and the
`CAP_SYS_ADMIN` capability. It depends upon the `/docker-volume-sshfs`
entrypoint and uses the `/run/docker/plugins/sshfs.sock` socket to communicate
with Docker Engine. This plugin has no runtime parameters.
#### Creating the plugin
A new plugin can be created by running
`docker plugin create <plugin-name> ./path/to/plugin/data` where the plugin
data contains a plugin configuration file `config.json` and a root filesystem
in subdirectory `rootfs`.
After that the plugin `<plugin-name>` will show up in `docker plugin ls`.
Plugins can be pushed to remote registries with
`docker plugin push <plugin-name>`.
## Debugging plugins
Stdout of a plugin is redirected to dockerd logs. Such entries have a
`plugin=<ID>` suffix. Here are a few examples of commands for pluginID
`f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62` and their
corresponding log entries in the docker daemon logs.
```console
$ docker plugin install tiborvass/sample-volume-plugin
INFO[0036] Starting... Found 0 volumes on startup plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
```
```console
$ docker volume create -d tiborvass/sample-volume-plugin samplevol
INFO[0193] Create Called... Ensuring directory /data/samplevol exists on host... plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
INFO[0193] open /var/lib/docker/plugin-data/local-persist.json: no such file or directory plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
INFO[0193] Created volume samplevol with mountpoint /data/samplevol plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
INFO[0193] Path Called... Returned path /data/samplevol plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
```
```console
$ docker run -v samplevol:/tmp busybox sh
INFO[0421] Get Called... Found samplevol plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
INFO[0421] Mount Called... Mounted samplevol plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
INFO[0421] Path Called... Returned path /data/samplevol plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
INFO[0421] Unmount Called... Unmounted samplevol plugin=f52a3df433b9aceee436eaada0752f5797aab1de47e5485f1690a073b860ff62
```
#### Using runc to obtain logfiles and shell into the plugin.
Use `runc`, the default docker container runtime, for debugging plugins by
collecting plugin logs redirected to a file.
```console
$ sudo runc --root /run/docker/runtime-runc/plugins.moby list
ID PID STATUS BUNDLE CREATED OWNER
93f1e7dbfe11c938782c2993628c895cf28e2274072c4a346a6002446c949b25 15806 running /run/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby-plugins/93f1e7dbfe11c938782c2993628c895cf28e2274072c4a346a6002446c949b25 2018-02-08T21:40:08.621358213Z root
9b4606d84e06b56df84fadf054a21374b247941c94ce405b0a261499d689d9c9 14992 running /run/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby-plugins/9b4606d84e06b56df84fadf054a21374b247941c94ce405b0a261499d689d9c9 2018-02-08T21:35:12.321325872Z root
c5bb4b90941efcaccca999439ed06d6a6affdde7081bb34dc84126b57b3e793d 14984 running /run/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby-plugins/c5bb4b90941efcaccca999439ed06d6a6affdde7081bb34dc84126b57b3e793d 2018-02-08T21:35:12.321288966Z root
```
```console
$ sudo runc --root /run/docker/runtime-runc/plugins.moby exec 93f1e7dbfe11c938782c2993628c895cf28e2274072c4a346a6002446c949b25 cat /var/log/plugin.log
```
If the plugin has a built-in shell, then exec into the plugin can be done as
follows:
```console
$ sudo runc --root /run/docker/runtime-runc/plugins.moby exec -t 93f1e7dbfe11c938782c2993628c895cf28e2274072c4a346a6002446c949b25 sh
```
#### Using curl to debug plugin socket issues.
To verify if the plugin API socket that the docker daemon communicates with
is responsive, use curl. In this example, we will make API calls from the
docker host to volume and network plugins using curl 7.47.0 to ensure that
the plugin is listening on the said socket. For a well functioning plugin,
these basic requests should work. Note that plugin sockets are available on the host under `/var/run/docker/plugins/<pluginID>`
```console
$ curl -H "Content-Type: application/json" -XPOST -d '{}' --unix-socket /var/run/docker/plugins/e8a37ba56fc879c991f7d7921901723c64df6b42b87e6a0b055771ecf8477a6d/plugin.sock http:/VolumeDriver.List
{"Mountpoint":"","Err":"","Volumes":[{"Name":"myvol1","Mountpoint":"/data/myvol1"},{"Name":"myvol2","Mountpoint":"/data/myvol2"}],"Volume":null}
```
```console
$ curl -H "Content-Type: application/json" -XPOST -d '{}' --unix-socket /var/run/docker/plugins/45e00a7ce6185d6e365904c8bcf62eb724b1fe307e0d4e7ecc9f6c1eb7bcdb70/plugin.sock http:/NetworkDriver.GetCapabilities
{"Scope":"local"}
```
When using curl 7.5 and above, the URL should be of the form
`http://hostname/APICall`, where `hostname` is the valid hostname where the
plugin is installed and `APICall` is the call to the plugin API.
For example, `http://localhost/VolumeDriver.List`
+221 -1
View File
@@ -4,4 +4,224 @@ keywords: "API, Usage, plugins, documentation, developer"
title: Plugin Config Version 1 of Plugin V2 title: Plugin Config Version 1 of Plugin V2
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. This document outlines the format of the V0 plugin configuration.
Plugin configs describe the various constituents of a Docker engine plugin.
Plugin configs can be serialized to JSON format with the following media types:
| Config Type | Media Type |
|-------------|-----------------------------------------|
| config | `application/vnd.docker.plugin.v1+json` |
## Config Field Descriptions
Config provides the base accessible fields for working with V0 plugin format in
the registry.
- `description` string
Description of the plugin
- `documentation` string
Link to the documentation about the plugin
- `interface` PluginInterface
Interface implemented by the plugins, struct consisting of the following fields:
- `types` string array
Types indicate what interface(s) the plugin currently implements.
Supported types:
- `docker.volumedriver/1.0`
- `docker.networkdriver/1.0`
- `docker.ipamdriver/1.0`
- `docker.authz/1.0`
- `docker.logdriver/1.0`
- `docker.metricscollector/1.0`
- `socket` string
Socket is the name of the socket the engine should use to communicate with the plugins.
the socket will be created in `/run/docker/plugins`.
- `entrypoint` string array
Entrypoint of the plugin, see [`ENTRYPOINT`](https://docs.docker.com/reference/dockerfile/#entrypoint)
- `workdir` string
Working directory of the plugin, see [`WORKDIR`](https://docs.docker.com/reference/dockerfile/#workdir)
- `network` PluginNetwork
Network of the plugin, struct consisting of the following fields:
- `type` string
Network type.
Supported types:
- `bridge`
- `host`
- `none`
- `mounts` PluginMount array
Mount of the plugin, struct consisting of the following fields.
See [`MOUNTS`](https://github.com/opencontainers/runtime-spec/blob/master/config.md#mounts).
- `name` string
Name of the mount.
- `description` string
Description of the mount.
- `source` string
Source of the mount.
- `destination` string
Destination of the mount.
- `type` string
Mount type.
- `options` string array
Options of the mount.
- `ipchost` Boolean
Access to host ipc namespace.
- `pidhost` Boolean
Access to host PID namespace.
- `propagatedMount` string
Path to be mounted as rshared, so that mounts under that path are visible to
Docker. This is useful for volume plugins. This path will be bind-mounted
outside of the plugin rootfs so it's contents are preserved on upgrade.
- `env` PluginEnv array
Environment variables of the plugin, struct consisting of the following fields:
- `name` string
Name of the environment variable.
- `description` string
Description of the environment variable.
- `value` string
Value of the environment variable.
- `args` PluginArgs
Arguments of the plugin, struct consisting of the following fields:
- `name` string
Name of the arguments.
- `description` string
Description of the arguments.
- `value` string array
Values of the arguments.
- `linux` PluginLinux
- `capabilities` string array
Capabilities of the plugin (Linux only), see list [`here`](https://github.com/opencontainers/runc/blob/master/libcontainer/SPEC.md#security)
- `allowAllDevices` Boolean
If `/dev` is bind mounted from the host, and allowAllDevices is set to true, the plugin will have `rwm` access to all devices on the host.
- `devices` PluginDevice array
Device of the plugin, (Linux only), struct consisting of the following fields.
See [`DEVICES`](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#devices).
- `name` string
Name of the device.
- `description` string
Description of the device.
- `path` string
Path of the device.
## Example Config
The following example shows the 'tiborvass/sample-volume-plugin' plugin config.
```json
{
"Args": {
"Description": "",
"Name": "",
"Settable": null,
"Value": null
},
"Description": "A sample volume plugin for Docker",
"Documentation": "https://docs.docker.com/engine/extend/plugins/",
"Entrypoint": [
"/usr/bin/sample-volume-plugin",
"/data"
],
"Env": [
{
"Description": "",
"Name": "DEBUG",
"Settable": [
"value"
],
"Value": "0"
}
],
"Interface": {
"Socket": "plugin.sock",
"Types": [
"docker.volumedriver/1.0"
]
},
"Linux": {
"Capabilities": null,
"AllowAllDevices": false,
"Devices": null
},
"Mounts": null,
"Network": {
"Type": ""
},
"PropagatedMount": "/data",
"User": {},
"Workdir": ""
}
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+91 -1
View File
@@ -6,4 +6,94 @@ description: "How to add additional functionality to Docker with plugins extensi
keywords: "Examples, Usage, plugins, docker, documentation, user guide" keywords: "Examples, Usage, plugins, docker, documentation, user guide"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. This document describes the Docker Engine plugins generally available in Docker
Engine. To view information on plugins managed by Docker,
refer to [Docker Engine plugin system](_index.md).
> [!NOTE]
> Legacy plugins are superseded by Docker Engine's managed plugin system.
> For plugins installed and managed by Docker, use
> [`docker plugin install`](https://docs.docker.com/reference/cli/docker/plugin/install/)
> and the [Docker Engine plugin system](_index.md). The third-party plugins
> listed on this page are provided for historical reference, and archived
> projects are marked accordingly.
You can extend the capabilities of the Docker Engine by loading third-party
plugins. This page explains the types of plugins and provides links to several
volume and network plugins for Docker.
## Types of plugins
Plugins extend Docker's functionality. They come in specific types. For
example, a [volume plugin](plugins_volume.md) might enable Docker
volumes to persist across multiple Docker hosts and a
[network plugin](plugins_network.md) might provide network plumbing.
Currently Docker supports authorization, volume and network driver plugins. In the future it
will support additional plugin types.
## Installing a plugin
Follow the instructions in the plugin's documentation.
## Finding a plugin
The sections below provide an overview of third-party plugins that use the
legacy plugin model.
### Network plugins
| Plugin | Description |
| :--------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Contiv Networking](https://github.com/contiv/netplugin) | An open source network plugin to provide infrastructure and security policies for a multi-tenant micro services deployment, while providing an integration to physical network for non-container workload. Contiv Networking implements the remote driver and IPAM APIs available in Docker 1.9 onwards. |
| [Kuryr Network Plugin](https://github.com/openstack/kuryr) | A network plugin is developed as part of the OpenStack Kuryr project and implements the Docker networking (libnetwork) remote driver API by utilizing Neutron, the OpenStack networking service. It includes an IPAM driver as well. |
| [Kathará Network Plugin](https://github.com/KatharaFramework/NetworkPlugin) | Docker Network Plugin used by Kathará, an open source container-based network emulation system for showing interactive demos/lessons, testing production networks in a sandbox environment, or developing new network protocols. |
### Volume plugins
| Plugin | Description |
|:---------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Azure File Storage plugin](https://github.com/Azure/azurefile-dockervolumedriver) (archived) | Lets you mount Microsoft [Azure File Storage](https://azure.microsoft.com/blog/azure-file-storage-now-generally-available/) shares to Docker containers as volumes using the SMB 3.0 protocol. [Learn more](https://azure.microsoft.com/blog/persistent-docker-volumes-with-azure-file-storage/). |
| [BeeGFS Volume Plugin](https://github.com/RedCoolBeans/docker-volume-beegfs) | An open source volume plugin to create persistent volumes in a BeeGFS parallel file system. |
| [Blockbridge plugin](https://github.com/blockbridge/blockbridge-docker-volume) | A volume plugin that provides access to an extensible set of container-based persistent storage options. It supports single and multi-host Docker environments with features that include tenant isolation, automated provisioning, encryption, secure deletion, snapshots and QoS. |
| [Contiv Volume Plugin](https://github.com/contiv/volplugin) | An open source volume plugin that provides multi-tenant, persistent, distributed storage with intent based consumption. It has support for Ceph and NFS. |
| [Convoy plugin](https://github.com/rancher/convoy) (archived) | A volume plugin for a variety of storage back-ends including device mapper and NFS. It's a simple standalone executable written in Go and provides the framework to support vendor-specific extensions such as snapshots, backups and restore. |
| [DigitalOcean Block Storage plugin](https://github.com/omallo/docker-volume-plugin-dostorage) | Integrates DigitalOcean's [block storage solution](https://www.digitalocean.com/products/storage/) into the Docker ecosystem by automatically attaching a given block storage volume to a DigitalOcean droplet and making the contents of the volume available to Docker containers running on that droplet. |
| [DRBD plugin](https://www.drbd.org/en/supported-projects/docker) | A volume plugin that provides highly available storage replicated by [DRBD](https://www.drbd.org). Data written to the docker volume is replicated in a cluster of DRBD nodes. |
| [Flocker plugin](https://github.com/ScatterHQ/flocker) (archived) | A volume plugin that provides multi-host portable volumes for Docker, enabling you to run databases and other stateful containers and move them around across a cluster of machines. |
| [Fuxi Volume Plugin](https://github.com/openstack-archive/fuxi) (archived) | A volume plugin that is developed as part of the OpenStack Kuryr project and implements the Docker volume plugin API by utilizing Cinder, the OpenStack block storage service. |
| [gce-docker plugin](https://github.com/mcuadros/gce-docker) (archived) | A volume plugin able to attach, format and mount Google Compute [persistent-disks](https://cloud.google.com/compute/docs/disks/persistent-disks). |
| [GlusterFS plugin](https://github.com/calavera/docker-volume-glusterfs) (archived) | A volume plugin that provides multi-host volumes management for Docker using GlusterFS. |
| [Horcrux Volume Plugin](https://github.com/muthu-r/horcrux) | A volume plugin that allows on-demand, version controlled access to your data. Horcrux is an open-source plugin, written in Go, and supports SCP, [Minio](https://www.minio.io) and Amazon S3. |
| [HPE 3Par Volume Plugin](https://github.com/hpe-storage/python-hpedockerplugin/) | A volume plugin that supports HPE 3Par and StoreVirtual iSCSI storage arrays. |
| [IPFS Volume Plugin](https://github.com/vdemeester/docker-volume-ipfs) (archived) | An open source volume plugin that allows using an [ipfs](https://ipfs.io/) filesystem as a volume. |
| [Keywhiz plugin](https://github.com/calavera/docker-volume-keywhiz) (archived) | A plugin that provides credentials and secret management using Keywhiz as a central repository. |
| [Linode Volume Plugin](https://github.com/linode/docker-volume-linode) | A plugin that adds the ability to manage Linode Block Storage as Docker Volumes from within a Linode. |
| [Local Persist Plugin](https://github.com/CWSpear/local-persist) | A volume plugin that extends the default `local` driver's functionality by allowing you specify a mountpoint anywhere on the host, which enables the files to *always persist*, even if the volume is removed via `docker volume rm`. |
| [NetApp Plugin](https://github.com/NetApp/netappdvp) (nDVP) | A volume plugin that provides direct integration with the Docker ecosystem for the NetApp storage portfolio. The nDVP package supports the provisioning and management of storage resources from the storage platform to Docker hosts, with a robust framework for adding additional platforms in the future. |
| [Netshare plugin](https://github.com/ContainX/docker-volume-netshare) | A volume plugin that provides volume management for NFS 3/4, AWS EFS and CIFS file systems. |
| [Nimble Storage Volume Plugin](https://scod.hpedev.io/docker_volume_plugins/hpe_nimble_storage/index.html) | A volume plug-in that integrates with Nimble Storage Unified Flash Fabric arrays. The plug-in abstracts array volume capabilities to the Docker administrator to allow self-provisioning of secure multi-tenant volumes and clones. |
| [OpenStorage Plugin](https://github.com/libopenstorage/openstorage) | A cluster-aware volume plugin that provides volume management for file and block storage solutions. It implements a vendor neutral specification for implementing extensions such as CoS, encryption, and snapshots. It has example drivers based on FUSE, NFS, NBD and EBS to name a few. |
| [Portworx Volume Plugin](https://github.com/portworx/px-dev) | A volume plugin that turns any server into a scale-out converged compute/storage node, providing container granular storage and highly available volumes across any node, using a shared-nothing storage backend that works with any docker scheduler. |
| [Quobyte Volume Plugin](https://github.com/quobyte/docker-volume) | A volume plugin that connects Docker to [Quobyte](https://www.quobyte.com/containers)'s data center file system, a general-purpose scalable and fault-tolerant storage platform. |
| [REX-Ray plugin](https://github.com/emccode/rexray) | A volume plugin which is written in Go and provides advanced storage functionality for many platforms including VirtualBox, EC2, Google Compute Engine, OpenStack, and EMC. |
| [Virtuozzo Storage and Ploop plugin](https://github.com/virtuozzo/docker-volume-ploop) | A volume plugin with support for Virtuozzo Storage distributed cloud file system as well as ploop devices. |
| [VMware vSphere Storage Plugin](https://github.com/vmware/docker-volume-vsphere) | Docker Volume Driver for vSphere enables customers to address persistent storage requirements for Docker containers in vSphere environments. |
### Authorization plugins
| Plugin | Description |
|:---------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Casbin AuthZ Plugin](https://github.com/casbin/casbin-authz-plugin) | An authorization plugin based on [Casbin](https://github.com/casbin/casbin), which supports access control models like ACL, RBAC, ABAC. The access control model can be customized. The policy can be persisted into file or DB. |
| [HBM plugin](https://github.com/kassisol/hbm) | An authorization plugin that prevents from executing commands with certains parameters. |
| [Twistlock AuthZ Broker](https://github.com/twistlock/authz) | A basic extendable authorization plugin that runs directly on the host or inside a container. This plugin allows you to define user policies that it evaluates during authorization. Basic authorization is provided if Docker daemon is started with the --tlsverify flag (username is extracted from the certificate common name). |
## Troubleshooting a plugin
If you are having problems with Docker after loading a plugin, ask the authors
of the plugin for help. The Docker team may not be able to assist you.
## Writing a plugin
If you are interested in writing a plugin for Docker, or seeing how they work
under the hood, see the [Docker plugins reference](plugin_api.md).
+180 -1
View File
@@ -4,4 +4,183 @@ description: "How to write Docker plugins extensions "
keywords: "API, Usage, plugins, documentation, developer" keywords: "API, Usage, plugins, documentation, developer"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. Docker plugins are out-of-process extensions which add capabilities to the
Docker Engine.
This document describes the Docker Engine plugin API. To view information on
plugins managed by Docker Engine, refer to [Docker Engine plugin system](_index.md).
This page is intended for people who want to develop their own Docker plugin.
If you just want to learn about or use Docker plugins, look
[here](legacy_plugins.md).
## What plugins are
A plugin is a process running on the same or a different host as the Docker daemon,
which registers itself by placing a file on the daemon host in one of the plugin
directories described in [Plugin discovery](#plugin-discovery).
Plugins have human-readable names, which are short, lowercase strings. For
example, `myplugin`.
Plugins can run inside or outside containers. Currently running them outside
containers is recommended.
## Plugin discovery
Docker discovers plugins by looking for them in the plugin directory whenever a
user or container tries to use one by name.
There are three types of files which can be put in the plugin directory.
* `.sock` files are Unix domain sockets.
* `.spec` files are text files containing a URL, such as `unix:///other.sock` or `tcp://localhost:8080`.
* `.json` files are text files containing a full json specification for the plugin.
Plugins with Unix domain socket files must run on the same host as the Docker daemon.
Plugins with `.spec` or `.json` files can run on a different host if you specify a remote URL.
Unix domain socket files must be located under `/run/docker/plugins`, whereas
spec files can be located either under `/etc/docker/plugins` or `/usr/lib/docker/plugins`.
The name of the file (excluding the extension) determines the plugin name.
For example, a plugin named `myplugin` might create a Unix socket at
`/run/docker/plugins/myplugin.sock`.
You can define each plugin into a separated subdirectory if you want to isolate definitions from each other.
For example, you can create the `myplugin` socket under `/run/docker/plugins/myplugin/myplugin.sock` and only
mount `/run/docker/plugins/myplugin` inside the `myplugin` container.
Docker always searches for Unix sockets in `/run/docker/plugins` first. It checks for spec or json files under
`/etc/docker/plugins` and `/usr/lib/docker/plugins` if the socket doesn't exist. The directory scan stops as
soon as it finds the first plugin definition with the given name.
### JSON specification
This is the JSON format for a plugin:
```json
{
"Name": "plugin-example",
"Addr": "https://example.com/docker/plugin",
"TLSConfig": {
"InsecureSkipVerify": false,
"CAFile": "/usr/shared/docker/certs/example-ca.pem",
"CertFile": "/usr/shared/docker/certs/example-cert.pem",
"KeyFile": "/usr/shared/docker/certs/example-key.pem"
}
}
```
The `TLSConfig` field is optional and TLS will only be verified if this configuration is present.
## Plugin lifecycle
Plugins should be started before Docker, and stopped after Docker. For
example, when packaging a plugin for a platform which supports `systemd`, you
might use [`systemd` dependencies](
https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before=) to
manage startup and shutdown order.
When upgrading a plugin, you should first stop the Docker daemon, upgrade the
plugin, then start Docker again.
## Plugin activation
When a plugin is first referred to -- either by a user referring to it by name
(e.g. `docker run --volume-driver=foo`) or a container already configured to
use a plugin being started -- Docker looks for the named plugin in the plugin
directory and activates it with a handshake. See Handshake API below.
Plugins are not activated automatically at Docker daemon startup. Rather,
they are activated only lazily, or on-demand, when they are needed.
## Systemd socket activation
Plugins may also be socket activated by `systemd`. The official [Plugins helpers](https://github.com/docker/go-plugins-helpers)
natively supports socket activation. In order for a plugin to be socket activated it needs
a `service` file and a `socket` file.
The `service` file (for example `/lib/systemd/system/your-plugin.service`):
```systemd
[Unit]
Description=Your plugin
Before=docker.service
After=network.target your-plugin.socket
Requires=your-plugin.socket docker.service
[Service]
ExecStart=/usr/lib/docker/your-plugin
[Install]
WantedBy=multi-user.target
```
The `socket` file (for example `/lib/systemd/system/your-plugin.socket`):
```systemd
[Unit]
Description=Your plugin
[Socket]
ListenStream=/run/docker/plugins/your-plugin.sock
[Install]
WantedBy=sockets.target
```
This will allow plugins to be actually started when the Docker daemon connects to
the sockets they're listening on (for instance the first time the daemon uses them
or if one of the plugin goes down accidentally).
## API design
The Plugin API is RPC-style JSON over HTTP, much like webhooks.
Requests flow from the Docker daemon to the plugin. The plugin needs to
implement an HTTP server and bind this to the Unix socket mentioned in the
"plugin discovery" section.
All requests are HTTP `POST` requests.
The API is versioned via an Accept header, which currently is always set to
`application/vnd.docker.plugins.v1+json`.
## Handshake API
Plugins are activated via the following "handshake" API call.
### /Plugin.Activate
Request: empty body
Response:
```json
{
"Implements": ["VolumeDriver"]
}
```
Responds with a list of Docker subsystems which this plugin implements.
After activation, the plugin will then be sent events from this subsystem.
Possible values are:
* [`authz`](plugins_authorization.md)
* [`NetworkDriver`](plugins_network.md)
* [`VolumeDriver`](plugins_volume.md)
## Plugin retries
Attempts to call a method on a plugin are retried with an exponential backoff
for up to 30 seconds. This may help when packaging plugins as containers, since
it gives plugin containers a chance to start up before failing any user
containers which depend on them.
## Plugins helpers
To ease plugins development, we're providing an `sdk` for each kind of plugins
currently supported by Docker at [docker/go-plugins-helpers](https://github.com/docker/go-plugins-helpers).
+277 -1
View File
@@ -6,4 +6,280 @@ aliases:
- "/engine/extend/authorization/" - "/engine/extend/authorization/"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. This document describes the Docker Engine plugins available in Docker
Engine. To view information on plugins managed by Docker Engine,
refer to [Docker Engine plugin system](_index.md).
Docker's out-of-the-box authorization model is all or nothing. Any user with
permission to access the Docker daemon can run any Docker client command. The
same is true for callers using Docker's Engine API to contact the daemon. If you
require greater access control, you can create authorization plugins and add
them to your Docker daemon configuration. Using an authorization plugin, a
Docker administrator can configure granular access policies for managing access
to the Docker daemon.
Anyone with the appropriate skills can develop an authorization plugin. These
skills, at their most basic, are knowledge of Docker, understanding of REST, and
sound programming knowledge. This document describes the architecture, state,
and methods information available to an authorization plugin developer.
## Basic principles
Docker's [plugin infrastructure](plugin_api.md) enables
extending Docker by loading, removing and communicating with
third-party components using a generic API. The access authorization subsystem
was built using this mechanism.
Using this subsystem, you don't need to rebuild the Docker daemon to add an
authorization plugin. You can add a plugin to an installed Docker daemon. You do
need to restart the Docker daemon to add a new plugin.
An authorization plugin approves or denies requests to the Docker daemon based
on both the current authentication context and the command context. The
authentication context contains all user details and the authentication method.
The command context contains all the relevant request data.
Authorization plugins must follow the rules described in [Docker Plugin API](plugin_api.md).
Each plugin must reside within directories described under the
[Plugin discovery](plugin_api.md#plugin-discovery) section.
> [!NOTE]
> The abbreviations `AuthZ` and `AuthN` mean authorization and authentication
> respectively.
## Default user authorization mechanism
If TLS is enabled in the [Docker daemon](https://docs.docker.com/engine/security/https/), the default user authorization flow extracts the user details from the certificate subject name.
That is, the `User` field is set to the client certificate subject common name, and the `AuthenticationMethod` field is set to `TLS`.
## Basic architecture
You are responsible for registering your plugin as part of the Docker daemon
startup. You can install multiple plugins and chain them together. This chain
can be ordered. Each request to the daemon passes in order through the chain.
Only when all the plugins grant access to the resource, is the access granted.
When an HTTP request is made to the Docker daemon through the CLI or via the
Engine API, the authentication subsystem passes the request to the installed
authentication plugin(s). The request contains the user (caller) and command
context. The plugin is responsible for deciding whether to allow or deny the
request.
The sequence diagrams below depict an allow and deny authorization flow:
![Authorization Allow flow](images/authz_allow.png)
![Authorization Deny flow](images/authz_deny.png)
Each request sent to the plugin includes the authenticated user, the HTTP
headers, and the request/response body. Only the user name and the
authentication method used are passed to the plugin. Most importantly, no user
credentials or tokens are passed.
> [!NOTE]
> Authorization plugins enforce requests to the Docker daemon's HTTP API only. gRPC method
> calls, whether dispatched natively or upgraded through `POST /grpc`, are not subject to authorization.
> Furthermore, HTTP request/response bodies where the `Content-Type` is `application/json` are forwarded;
> bodies of any other type are not visible to the plugin and cannot be used for enforcement,
> even though the daemon acts on this data.
For commands that can potentially hijack the HTTP connection (`HTTP
Upgrade`), such as `exec`, the authorization plugin is only called for the
initial HTTP requests. Once the plugin approves the command, authorization is
not applied to the rest of the flow. Specifically, the streaming data is not
passed to the authorization plugins. For commands that return chunked HTTP
response, such as `logs` and `events`, only the HTTP request is sent to the
authorization plugins.
The Engine's authorization middleware fails closed: when a plugin returns an error or returns `Allow: false`,
the request is denied and the error is surfaced to the client. Plugins should also fail closed: if the plugin
cannot confidently evaluate a request, it should return an error or `Allow: false`.
> [!WARNING]
> Because the plugin receives the [**raw** request body](#authzpluginauthzreq) from the daemon, it must
> apply the same decoding semantics as the daemon to be sure it evaluates the request the daemon will
> act on. The daemon decodes JSON with Go's [`encoding/json.Unmarshal`](https://pkg.go.dev/encoding/json#Unmarshal).
>
> The same requirement applies to the response body. Plugins that depend on `ResponseBody`
> inspection for redaction or content-filtering should restrict their policies to endpoints
> whose response is produced as a single write (typical of REST-style API responses). For
> commands whose responses are streamed or are likely to exceed the [buffer](#response-body-size-and-partial-buffering) through multiple
> writes, do not rely on `ResponseBody` for security-relevant decisions; perform the filtering
> in a separate layer in front of the daemon.
### Response body size and partial buffering
The internal buffer that holds the response body between the daemon's HTTP
handler and the plugin's response authorization callback (`responseModifier`,
defined in [`pkg/authorization/response.go`](https://github.com/moby/moby/blob/master/pkg/authorization/response.go))
has a fixed capacity of 64 KiB (`maxBufferSize`).
For most non-streaming endpoints the full response is buffered for plugin
inspection regardless of total size, because Go's `encoding/json` encoder
serializes the complete payload into a single underlying write. The
streaming-response exclusion noted above (for example, `logs` and `events`)
is the practical effect of this 64 KiB threshold combined with the
`io.WriteFlusher` write pattern used by streaming handlers, where each write
is immediately drained to the client and is therefore no longer available
for plugin inspection by the time the handler returns.
During request/response processing, some authorization flows might
need to do additional queries to the Docker daemon. To complete such flows,
plugins can call the daemon API similar to a regular user. To enable these
additional queries, the plugin must provide the means for an administrator to
configure proper authentication and security policies.
## Docker client flows
To enable and configure the authorization plugin, the plugin developer must
support the Docker client interactions detailed in this section.
### Setting up Docker daemon
Enable the authorization plugin with a dedicated command line flag in the
`--authorization-plugin=PLUGIN_ID` format. The flag supplies a `PLUGIN_ID`
value. This value can be the plugin’s socket or a path to a specification file.
Authorization plugins can be loaded without restarting the daemon. Refer
to the [`dockerd` documentation](https://docs.docker.com/reference/cli/dockerd/#configuration-reload-behavior) for more information.
```console
$ dockerd --authorization-plugin=plugin1 --authorization-plugin=plugin2,...
```
Docker's authorization subsystem supports multiple `--authorization-plugin` parameters.
### Calling authorized command (allow)
```console
$ docker pull centos
<...>
f1b10cd84249: Pull complete
<...>
```
### Calling unauthorized command (deny)
```console
$ docker pull centos
<...>
docker: Error response from daemon: authorization denied by plugin PLUGIN_NAME: volumes are not allowed.
```
### Error from plugins
```console
$ docker pull centos
<...>
docker: Error response from daemon: plugin PLUGIN_NAME failed with error: AuthZPlugin.AuthZReq: Cannot connect to the Docker daemon. Is the docker daemon running on this host?.
```
## API schema and implementation
In addition to Docker's standard plugin registration method, each plugin
should implement the following two methods:
* `/AuthZPlugin.AuthZReq` This authorize request method is called before the Docker daemon processes the client request.
* `/AuthZPlugin.AuthZRes` This authorize response method is called before the response is returned from Docker daemon to the client.
#### /AuthZPlugin.AuthZReq
Request
```json
{
"User": "The user identification",
"UserAuthNMethod": "The authentication method used",
"RequestMethod": "The HTTP method",
"RequestURI": "The HTTP request URI",
"RequestBody": "Byte array containing the raw HTTP request body",
"RequestHeader": "Byte array containing the raw HTTP request header as a map[string][]string "
}
```
Response
```json
{
"Allow": "Determined whether the user is allowed or not",
"Msg": "The authorization message",
"Err": "The error message if things go wrong"
}
```
#### /AuthZPlugin.AuthZRes
Request:
```json
{
"User": "The user identification",
"UserAuthNMethod": "The authentication method used",
"RequestMethod": "The HTTP method",
"RequestURI": "The HTTP request URI",
"RequestBody": "Byte array containing the raw HTTP request body",
"RequestHeader": "Byte array containing the raw HTTP request header as a map[string][]string",
"ResponseBody": "Byte array containing the raw HTTP response body",
"ResponseHeader": "Byte array containing the raw HTTP response header as a map[string][]string",
"ResponseStatusCode":"Response status code"
}
```
Response:
```json
{
"Allow": "Determined whether the user is allowed or not",
"Msg": "The authorization message",
"Err": "The error message if things go wrong"
}
```
### Request authorization
Each plugin must support two request authorization messages formats, one from the daemon to the plugin and then from the plugin to the daemon. The tables below detail the content expected in each message.
#### Daemon -> Plugin
Name | Type | Description
-----------------------|-------------------|-------------------------------------------------------
User | string | The user identification
Authentication method | string | The authentication method used
Request method | enum | The HTTP method (GET/DELETE/POST)
Request URI | string | The HTTP request URI including API version, as sent by the client (e.g., v.1.17/containers/json)
Request headers | map[string]string | Request headers as key value pairs (without the authorization header)
Request body | []byte | Raw request body
#### Plugin -> Daemon
Name | Type | Description
--------|--------|----------------------------------------------------------------------------------
Allow | bool | Boolean value indicating whether the request is allowed or denied
Msg | string | Authorization message (will be returned to the client in case the access is denied)
Err | string | Error message (will be returned to the client in case the plugin encounter an error. The string value supplied may appear in logs, so should not include confidential information)
### Response authorization
The plugin must support two authorization messages formats, one from the daemon to the plugin and then from the plugin to the daemon. The tables below detail the content expected in each message.
#### Daemon -> Plugin
Name | Type | Description
----------------------- |------------------ |----------------------------------------------------
User | string | The user identification
Authentication method | string | The authentication method used
Request method | string | The HTTP method (GET/DELETE/POST)
Request URI | string | The HTTP request URI including API version, as sent by the client (e.g., v.1.17/containers/json)
Request headers | map[string]string | Request headers as key value pairs (without the authorization header)
Request body | []byte | Raw request body
Response status code | int | Status code from the Docker daemon
Response headers | map[string]string | Response headers as key value pairs
Response body | []byte | Raw Docker daemon response body
#### Plugin -> Daemon
Name | Type | Description
--------|--------|----------------------------------------------------------------------------------
Allow | bool | Boolean value indicating whether the response is allowed or denied
Msg | string | Authorization message (will be returned to the client in case the access is denied)
Err | string | Error message (will be returned to the client in case the plugin encounter an error. The string value supplied may appear in logs, so should not include confidential information)
+209 -1
View File
@@ -4,4 +4,212 @@ description: "Log driver plugins."
keywords: "Examples, Usage, plugins, docker, documentation, user guide, logging" keywords: "Examples, Usage, plugins, docker, documentation, user guide, logging"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. This document describes logging driver plugins for Docker.
Logging drivers enables users to forward container logs to another service for
processing. Docker includes several logging drivers as built-ins, however can
never hope to support all use-cases with built-in drivers. Plugins allow Docker
to support a wide range of logging services without requiring to embed client
libraries for these services in the main Docker codebase. See the
[plugin documentation](legacy_plugins.md) for more information.
## Create a logging plugin
The main interface for logging plugins uses the same JSON+HTTP RPC protocol used
by other plugin types. See the
[example](https://github.com/cpuguy83/docker-log-driver-test) plugin for a
reference implementation of a logging plugin. The example wraps the built-in
`jsonfilelog` log driver.
## LogDriver protocol
Logging plugins must register as a `LogDriver` during plugin activation. Once
activated users can specify the plugin as a log driver.
There are two HTTP endpoints that logging plugins must implement:
### `/LogDriver.StartLogging`
Signals to the plugin that a container is starting that the plugin should start
receiving logs for.
Logs will be streamed over the defined file in the request. On Linux this file
is a FIFO. Logging plugins are not currently supported on Windows.
Request:
```json
{
"File": "/path/to/file/stream",
"Info": {
"ContainerID": "123456"
}
}
```
`File` is the path to the log stream that needs to be consumed. Each call to
`StartLogging` should provide a different file path, even if it's a container
that the plugin has already received logs for prior. The file is created by
Docker with a randomly generated name.
`Info` is details about the container that's being logged. This is fairly
free-form, but is defined by the following struct definition:
```go
type Info struct {
Config map[string]string
ContainerID string
ContainerName string
ContainerEntrypoint string
ContainerArgs []string
ContainerImageID string
ContainerImageName string
ContainerCreated time.Time
ContainerEnv []string
ContainerLabels map[string]string
LogPath string
DaemonName string
}
```
`ContainerID` will always be supplied with this struct, but other fields may be
empty or missing.
Response:
```json
{
"Err": ""
}
```
If an error occurred during this request, add an error message to the `Err` field
in the response. If no error then you can either send an empty response (`{}`)
or an empty value for the `Err` field.
The driver should at this point be consuming log messages from the passed in file.
If messages are unconsumed, it may cause the container to block while trying to
write to its stdio streams.
Log stream messages are encoded as protocol buffers. The protobuf definitions are
in the
[moby repository](https://github.com/moby/moby/blob/master/api/types/plugins/logdriver/entry.proto).
Since protocol buffers are not self-delimited you must decode them from the stream
using the following stream format:
```text
[size][message]
```
Where `size` is a 4-byte big endian binary encoded uint32. `size` in this case
defines the size of the next message. `message` is the actual log entry.
A reference golang implementation of a stream encoder/decoder can be found
[here](https://github.com/docker/docker/blob/master/api/types/plugins/logdriver/io.go)
### `/LogDriver.StopLogging`
Signals to the plugin to stop collecting logs from the defined file.
Once a response is received, the file will be removed by Docker. You must make
sure to collect all logs on the stream before responding to this request or risk
losing log data.
Requests on this endpoint does not mean that the container has been removed
only that it has stopped.
Request:
```json
{
"File": "/path/to/file/stream"
}
```
Response:
```json
{
"Err": ""
}
```
If an error occurred during this request, add an error message to the `Err` field
in the response. If no error then you can either send an empty response (`{}`)
or an empty value for the `Err` field.
## Optional endpoints
Logging plugins can implement two extra logging endpoints:
### `/LogDriver.Capabilities`
Defines the capabilities of the log driver. You must implement this endpoint for
Docker to be able to take advantage of any of the defined capabilities.
Request:
```json
{}
```
Response:
```json
{
"ReadLogs": true
}
```
Supported capabilities:
- `ReadLogs` - this tells Docker that the plugin is capable of reading back logs
to clients. Plugins that report that they support `ReadLogs` must implement the
`/LogDriver.ReadLogs` endpoint
### `/LogDriver.ReadLogs`
Reads back logs to the client. This is used when `docker logs <container>` is
called.
In order for Docker to use this endpoint, the plugin must specify as much when
`/LogDriver.Capabilities` is called.
Request:
```json
{
"ReadConfig": {},
"Info": {
"ContainerID": "123456"
}
}
```
`ReadConfig` is the list of options for reading, it is defined with the following
golang struct:
```go
type ReadConfig struct {
Since time.Time
Tail int
Follow bool
}
```
- `Since` defines the oldest log that should be sent.
- `Tail` defines the number of lines to read (e.g. like the command `tail -n 10`)
- `Follow` signals that the client wants to stay attached to receive new log messages
as they come in once the existing logs have been read.
`Info` is the same type defined in `/LogDriver.StartLogging`. It should be used
to determine what set of logs to read.
Response:
```text
{{ log stream }}
```
The response should be the encoded log message using the same format as the
messages that the plugin consumed from Docker.
+73 -1
View File
@@ -4,4 +4,76 @@ description: "Metrics plugins."
keywords: "Examples, Usage, plugins, docker, documentation, user guide, metrics" keywords: "Examples, Usage, plugins, docker, documentation, user guide, metrics"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. Docker exposes internal metrics based on the Prometheus format. Metrics plugins
enable accessing these metrics in a consistent way by providing a Unix
socket at a predefined path where the plugin can scrape the metrics.
> [!NOTE]
> While the plugin interface for metrics is non-experimental, the naming of the
> metrics and metric labels is still considered experimental and may change in a
> future version.
## Creating a metrics plugin
You must currently set `PropagatedMount` in the plugin `config.json` to
`/run/docker`. This allows the plugin to receive updated mounts
(the bind-mounted socket) from Docker after the plugin is already configured.
## MetricsCollector protocol
Metrics plugins must register as implementing the`MetricsCollector` interface
in `config.json`.
On Unix platforms, the socket is located at `/run/docker/metrics.sock` in the
plugin's rootfs.
`MetricsCollector` must implement two endpoints:
### `MetricsCollector.StartMetrics`
Signals to the plugin that the metrics socket is now available for scraping
Request:
```json
{}
```
The request has no payload.
Response:
```json
{
"Err": ""
}
```
If an error occurred during this request, add an error message to the `Err` field
in the response. If no error then you can either send an empty response (`{}`)
or an empty value for the `Err` field. Errors will only be logged.
### `MetricsCollector.StopMetrics`
Signals to the plugin that the metrics socket is no longer available.
This may happen when the daemon is shutting down.
Request:
```json
{}
```
The request has no payload.
Response:
```json
{
"Err": ""
}
```
If an error occurred during this request, add an error message to the `Err` field
in the response. If no error then you can either send an empty response (`{}`)
or an empty value for the `Err` field. Errors will only be logged.
+58 -1
View File
@@ -4,4 +4,61 @@ description: "Network driver plugins."
keywords: "Examples, Usage, plugins, docker, documentation, user guide" keywords: "Examples, Usage, plugins, docker, documentation, user guide"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. This document describes Docker Engine network driver plugins generally
available in Docker Engine. To view information on plugins
managed by Docker Engine, refer to [Docker Engine plugin system](_index.md).
Docker Engine network plugins enable Engine deployments to be extended to
support a wide range of networking technologies, such as VXLAN, IPVLAN, MACVLAN
or something completely different. Network driver plugins are supported via the
LibNetwork project. Each plugin is implemented as a "remote driver" for
LibNetwork, which shares plugin infrastructure with Engine. Effectively, network
driver plugins are activated in the same way as other plugins, and use the same
kind of protocol.
## Network plugins and Swarm mode
[Legacy plugins](legacy_plugins.md) do not work in Swarm mode. However,
plugins written using the [v2 plugin system](_index.md) do work in Swarm mode, as
long as they are installed on each Swarm worker node.
## Use network driver plugins
The means of installing and running a network driver plugin depend on the
particular plugin. So, be sure to install your plugin according to the
instructions obtained from the plugin developer.
Once running however, network driver plugins are used just like the built-in
network drivers: by being mentioned as a driver in network-oriented Docker
commands. For example,
```console
$ docker network create --driver weave mynet
```
Some network driver plugins are listed in [plugins](legacy_plugins.md)
The `mynet` network is now owned by `weave`, so subsequent commands
referring to that network will be sent to the plugin,
```console
$ docker run --network=mynet busybox top
```
## Find network plugins
Network plugins are written by third parties, and are published by those
third parties, either on
[Docker Hub](https://hub.docker.com/search?q=&type=plugin)
or on the third party's site.
## Write a network plugin
Network plugins implement the [Docker plugin API](plugin_api.md) and the network
plugin protocol
## Network plugin protocol
The network driver protocol, in addition to the plugin activation call, is
documented as part of libnetwork:
[https://github.com/moby/moby/blob/master/daemon/libnetwork/docs/remote.md](https://github.com/moby/moby/blob/master/daemon/libnetwork/docs/remote.md).
+179 -1
View File
@@ -3,4 +3,182 @@ keywords: "API, Usage, plugins, documentation, developer"
title: Plugins and Services title: Plugins and Services
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. <!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# Using Volume and Network plugins in Docker services
In swarm mode, it is possible to create a service that allows for attaching
to networks or mounting volumes that are backed by plugins. Swarm schedules
services based on plugin availability on a node.
### Volume plugins
In this example, a volume plugin is installed on a swarm worker and a volume
is created using the plugin. In the manager, a service is created with the
relevant mount options. It can be observed that the service is scheduled to
run on the worker node with the said volume plugin and volume. Note that,
node1 is the manager and node2 is the worker.
1. Prepare manager. In node 1:
```console
$ docker swarm init
Swarm initialized: current node (dxn1zf6l61qsb1josjja83ngz) is now a manager.
```
2. Join swarm, install plugin and create volume on worker. In node 2:
```console
$ docker swarm join \
--token SWMTKN-1-aabbccdd00112233aabbccdd00112233aabbccdd00112233aa-aabbccdd00112233... \
192.168.99.100:2377
```
```console
$ docker plugin install tiborvass/sample-volume-plugin
latest: Pulling from tiborvass/sample-volume-plugin
eb9c16fbdc53: Download complete
Digest: sha256:00b42de88f3a3e0342e7b35fa62394b0a9ceb54d37f4c50be5d3167899994639
Status: Downloaded newer image for tiborvass/sample-volume-plugin:latest
Installed plugin tiborvass/sample-volume-plugin
```
```console
$ docker volume create -d tiborvass/sample-volume-plugin --name pluginVol
```
3. Create a service using the plugin and volume. In node1:
```console
$ docker service create --name my-service --mount type=volume,volume-driver=tiborvass/sample-volume-plugin,source=pluginVol,destination=/tmp busybox top
$ docker service ls
z1sj8bb8jnfn my-service replicated 1/1 busybox:latest
```
`docker service ls` shows service 1 instance of service running.
4. Observe the task getting scheduled in node 2:
```console
$ docker ps --format '{{.ID}}\t {{.Status}} {{.Names}} {{.Command}}'
83fc1e842599 Up 2 days my-service.1.9jn59qzn7nbc3m0zt1hij12xs "top"
```
### Network plugins
In this example, a global scope network plugin is installed on both the
swarm manager and worker. A service is created with replicated instances
using the installed plugin. We will observe how the availability of the
plugin determines network creation and container scheduling.
Note that node1 is the manager and node2 is the worker.
1. Install a global scoped network plugin on both manager and worker. On node1
and node2:
```console
$ docker plugin install bboreham/weave2
Plugin "bboreham/weave2" is requesting the following privileges:
- network: [host]
- capabilities: [CAP_SYS_ADMIN CAP_NET_ADMIN]
Do you grant the above permissions? [y/N] y
latest: Pulling from bboreham/weave2
7718f575adf7: Download complete
Digest: sha256:2780330cc15644b60809637ee8bd68b4c85c893d973cb17f2981aabfadfb6d72
Status: Downloaded newer image for bboreham/weave2:latest
Installed plugin bboreham/weave2
```
2. Create a network using plugin on manager. On node1:
```console
$ docker network create --driver=bboreham/weave2:latest globalnet
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
qlj7ueteg6ly globalnet bboreham/weave2:latest swarm
```
3. Create a service on the manager and have replicas set to 8. Observe that
containers get scheduled on both manager and worker.
On node 1:
```console
$ docker service create --network globalnet --name myservice --replicas=8 mrjana/simpleweb simpleweb
w90drnfzw85nygbie9kb89vpa
```
```console
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
87520965206a mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 5 seconds ago Up 4 seconds myservice.4.ytdzpktmwor82zjxkh118uf1v
15e24de0f7aa mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 5 seconds ago Up 4 seconds myservice.2.kh7a9n3iauq759q9mtxyfs9hp
c8c8f0144cdc mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 5 seconds ago Up 4 seconds myservice.6.sjhpj5gr3xt33e3u2jycoj195
2e8e4b2c5c08 mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 5 seconds ago Up 4 seconds myservice.8.2z29zowsghx66u2velublwmrh
```
On node 2:
```console
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
53c0ae7c1dae mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 2 seconds ago Up Less than a second myservice.7.x44tvvdm3iwkt9kif35f7ykz1
9b56c627fee0 mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 2 seconds ago Up Less than a second myservice.1.x7n1rm6lltw5gja3ueikze57q
d4f5927ba52c mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 2 seconds ago Up 1 second myservice.5.i97bfo9uc6oe42lymafs9rz6k
478c0d395bd7 mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 2 seconds ago Up Less than a second myservice.3.yr7nkffa48lff1vrl2r1m1ucs
```
4. Scale down the number of instances. On node1:
```console
$ docker service scale myservice=0
myservice scaled to 0
```
5. Disable and uninstall the plugin on the worker. On node2:
```console
$ docker plugin rm -f bboreham/weave2
bboreham/weave2
```
6. Scale up the number of instances again. Observe that all containers are
scheduled on the master and not on the worker, because the plugin is not available on the worker anymore.
On node 1:
```console
$ docker service scale myservice=8
myservice scaled to 8
```
```console
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cf4b0ec2415e mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 39 seconds ago Up 36 seconds myservice.3.r7p5o208jmlzpcbm2ytl3q6n1
57c64a6a2b88 mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 39 seconds ago Up 36 seconds myservice.4.dwoezsbb02ccstkhlqjy2xe7h
3ac68cc4e7b8 mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 39 seconds ago Up 35 seconds myservice.5.zx4ezdrm2nwxzkrwnxthv0284
006c3cb318fc mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 39 seconds ago Up 36 seconds myservice.8.q0e3umt19y3h3gzo1ty336k5r
dd2ffebde435 mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 39 seconds ago Up 36 seconds myservice.7.a77y3u22prjipnrjg7vzpv3ba
a86c74d8b84b mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 39 seconds ago Up 36 seconds myservice.6.z9nbn14bagitwol1biveeygl7
2846a7850ba0 mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 39 seconds ago Up 37 seconds myservice.2.ypufz2eh9fyhppgb89g8wtj76
e2ec01efcd8a mrjana/simpleweb@sha256:317d7f221d68c86d503119b0ea12c29de42af0a22ca087d522646ad1069a47a4 "simpleweb" 39 seconds ago Up 38 seconds myservice.1.8w7c4ttzr6zcb9sjsqyhwp3yl
```
On node 2:
```console
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
+359 -1
View File
@@ -4,4 +4,362 @@ description: "How to manage data with external volume plugins"
keywords: "Examples, Usage, volume, docker, data, volumes, plugin, api" keywords: "Examples, Usage, volume, docker, data, volumes, plugin, api"
--- ---
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository. Docker Engine volume plugins enable Engine deployments to be integrated with
external storage systems such as Amazon EBS, and enable data volumes to persist
beyond the lifetime of a single Docker host. See the
[plugin documentation](legacy_plugins.md) for more information.
## Changelog
### 1.13.0
- If used as part of the v2 plugin architecture, mountpoints that are part of
paths returned by the plugin must be mounted under the directory specified by
`PropagatedMount` in the plugin configuration
([#26398](https://github.com/docker/docker/pull/26398))
### 1.12.0
- Add `Status` field to `VolumeDriver.Get` response
([#21006](https://github.com/docker/docker/pull/21006#))
- Add `VolumeDriver.Capabilities` to get capabilities of the volume driver
([#22077](https://github.com/docker/docker/pull/22077))
### 1.10.0
- Add `VolumeDriver.Get` which gets the details about the volume
([#16534](https://github.com/docker/docker/pull/16534))
- Add `VolumeDriver.List` which lists all volumes owned by the driver
([#16534](https://github.com/docker/docker/pull/16534))
### 1.8.0
- Initial support for volume driver plugins
([#14659](https://github.com/docker/docker/pull/14659))
## Command-line changes
To give a container access to a volume, use the `--volume` and `--volume-driver`
flags on the `docker container run` command. The `--volume` (or `-v`) flag
accepts a volume name and path on the host, and the `--volume-driver` flag
accepts a driver type.
```console
$ docker volume create --driver=myplugin volumename
$ docker container run -it --volume volumename:/data busybox sh
```
### `--volume`
The `--volume` (or `-v`) flag takes a value that is in the format
`<volume_name>:<mountpoint>`. The two parts of the value are
separated by a colon (`:`) character.
- The volume name is a human-readable name for the volume, and cannot begin with
a `/` character. It is referred to as `volume_name` in the rest of this topic.
- The `Mountpoint` is the path on the host (v1) or in the plugin (v2) where the
volume has been made available.
### `--volume-driver`
Specifying the `--volume-driver` flag together with a volume name (using
`--volume`) allows you to use plugins to manage volumes for the container.
The `--volume-driver` flag is used as a default for all volumes created for
the container, including anonymous volumes. Use the [`--mount`] flag with
the [`volume-driver`] option to specify the driver to use for each volume
individually.
[`--mount`]: https://docs.docker.com/reference/cli/docker/container/run/#mount
[`volume-driver`]: https://docs.docker.com/engine/storage/volumes/#start-a-container-which-creates-a-volume-using-a-volume-driver
## Create a VolumeDriver
The container creation endpoint (`/containers/create`) accepts a `VolumeDriver`
field of type `string` allowing to specify the name of the driver. If not
specified, it defaults to `"local"` (the default driver for local volumes).
## Volume plugin protocol
If a plugin registers itself as a `VolumeDriver` when activated, it must
provide the Docker Daemon with writeable paths on the host filesystem. The Docker
daemon provides these paths to containers to consume. The Docker daemon makes
the volumes available by bind-mounting the provided paths into the containers.
> [!NOTE]
> Volume plugins should *not* write data to the `/var/lib/docker/` directory,
> including `/var/lib/docker/volumes`. The `/var/lib/docker/` directory is
> reserved for Docker.
### `/VolumeDriver.Create`
Request:
```json
{
"Name": "volume_name",
"Opts": {}
}
```
Instruct the plugin that the user wants to create a volume, given a user
specified volume name. The plugin does not need to actually manifest the
volume on the filesystem yet (until `Mount` is called).
`Opts` is a map of driver specific options passed through from the user request.
Response:
```json
{
"Err": ""
}
```
Respond with a string error if an error occurred.
### `/VolumeDriver.Remove`
Request:
```json
{
"Name": "volume_name"
}
```
Delete the specified volume from disk. This request is issued when a user
invokes `docker rm -v` to remove volumes associated with a container.
Response:
```json
{
"Err": ""
}
```
Respond with a string error if an error occurred.
### `/VolumeDriver.Mount`
Request:
```json
{
"Name": "volume_name",
"ID": "b87d7442095999a92b65b3d9691e697b61713829cc0ffd1bb72e4ccd51aa4d6c"
}
```
Docker requires the plugin to provide a volume, given a user specified volume
name. `Mount` is called once per container start. If the same `volume_name` is requested
more than once, the plugin may need to keep track of each new mount request and provision
at the first mount request and deprovision at the last corresponding unmount request.
`ID` is a unique ID for the caller that is requesting the mount.
Response:
- v1
```json
{
"Mountpoint": "/path/to/directory/on/host",
"Err": ""
}
```
- v2
```json
{
"Mountpoint": "/path/under/PropagatedMount",
"Err": ""
}
```
`Mountpoint` is the path on the host (v1) or in the plugin (v2) where the volume
has been made available.
`Err` is either empty or contains an error string.
### `/VolumeDriver.Path`
Request:
```json
{
"Name": "volume_name"
}
```
Request the path to the volume with the given `volume_name`.
Response:
- v1
```json
{
"Mountpoint": "/path/to/directory/on/host",
"Err": ""
}
```
- v2
```json
{
"Mountpoint": "/path/under/PropagatedMount",
"Err": ""
}
```
Respond with the path on the host (v1) or inside the plugin (v2) where the
volume has been made available, and/or a string error if an error occurred.
`Mountpoint` is optional. However, the plugin may be queried again later if one
is not provided.
### `/VolumeDriver.Unmount`
Request:
```json
{
"Name": "volume_name",
"ID": "b87d7442095999a92b65b3d9691e697b61713829cc0ffd1bb72e4ccd51aa4d6c"
}
```
Docker is no longer using the named volume. `Unmount` is called once per
container stop. Plugin may deduce that it is safe to deprovision the volume at
this point.
`ID` is a unique ID for the caller that is requesting the mount.
Response:
```json
{
"Err": ""
}
```
Respond with a string error if an error occurred.
### `/VolumeDriver.Get`
Request:
```json
{
"Name": "volume_name"
}
```
Get info about `volume_name`.
Response:
- v1
```json
{
"Volume": {
"Name": "volume_name",
"Mountpoint": "/path/to/directory/on/host",
"Status": {}
},
"Err": ""
}
```
- v2
```json
{
"Volume": {
"Name": "volume_name",
"Mountpoint": "/path/under/PropagatedMount",
"Status": {}
},
"Err": ""
}
```
Respond with a string error if an error occurred. `Mountpoint` and `Status` are
optional.
### /VolumeDriver.List
Request:
```json
{}
```
Get the list of volumes registered with the plugin.
Response:
- v1
```json
{
"Volumes": [
{
"Name": "volume_name",
"Mountpoint": "/path/to/directory/on/host"
}
],
"Err": ""
}
```
- v2
```json
{
"Volumes": [
{
"Name": "volume_name",
"Mountpoint": "/path/under/PropagatedMount"
}
],
"Err": ""
}
```
Respond with a string error if an error occurred. `Mountpoint` is optional.
### /VolumeDriver.Capabilities
Request:
```json
{}
```
Get the list of capabilities the driver supports.
The driver is not required to implement `Capabilities`. If it is not
implemented, the default values are used.
Response:
```json
{
"Capabilities": {
"Scope": "global"
}
}
```
Supported scopes are `global` and `local`. Any other value in `Scope` will be
ignored, and `local` is used. `Scope` allows cluster managers to handle the
volume in different ways. For instance, a scope of `global`, signals to the
cluster manager that it only needs to create the volume once instead of on each
Docker host. More capabilities may be added in the future.
@@ -44,8 +44,8 @@ created. Supported `Dockerfile` instructions:
$ docker ps $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton 197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker commit c3f279d17e0a svendowideit/testimage:version3 $ docker commit c3f279d17e0a svendowideit/testimage:version3
@@ -63,8 +63,8 @@ svendowideit/testimage version3 f5283438590d 16 sec
$ docker ps $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton 197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker inspect -f "{{ .Config.Env }}" c3f279d17e0a $ docker inspect -f "{{ .Config.Env }}" c3f279d17e0a
@@ -85,8 +85,8 @@ $ docker inspect -f "{{ .Config.Env }}" f5283438590d
$ docker ps $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton 197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker commit --change='CMD ["apachectl", "-DFOREGROUND"]' -c "EXPOSE 80" c3f279d17e0a svendowideit/testimage:version4 $ docker commit --change='CMD ["apachectl", "-DFOREGROUND"]' -c "EXPOSE 80" c3f279d17e0a svendowideit/testimage:version4
@@ -100,6 +100,6 @@ $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
89373736e2e7 testimage:version4 "apachectl -DFOREGROU" 3 seconds ago Up 2 seconds 80/tcp distracted_fermat 89373736e2e7 testimage:version4 "apachectl -DFOREGROU" 3 seconds ago Up 2 seconds 80/tcp distracted_fermat
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton 197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
``` ```
+5 -5
View File
@@ -33,7 +33,7 @@ Running `docker ps --no-trunc` showing 2 linked containers.
$ docker ps --no-trunc $ docker ps --no-trunc
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:26.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:24.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp
9ca9747b233100676a48cc7806131586213fa5dab86dd1972d6a8732e3a84a4d crosbymichael/redis:latest /redis-server --dir 33 minutes ago Up 33 minutes 6379/tcp redis,webapp/db 9ca9747b233100676a48cc7806131586213fa5dab86dd1972d6a8732e3a84a4d crosbymichael/redis:latest /redis-server --dir 33 minutes ago Up 33 minutes 6379/tcp redis,webapp/db
``` ```
@@ -240,13 +240,13 @@ CONTAINER ID IMAGE COMMAND CREATED
919e1179bdb8 ubuntu-c1 "top" About a minute ago Up About a minute admiring_lovelace 919e1179bdb8 ubuntu-c1 "top" About a minute ago Up About a minute admiring_lovelace
``` ```
Match containers based on the `ubuntu` version `26.04` image: Match containers based on the `ubuntu` version `24.04` image:
```console ```console
$ docker ps --filter ancestor=ubuntu:26.04 $ docker ps --filter ancestor=ubuntu:24.04
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
82a598284012 ubuntu:26.04 "top" 3 minutes ago Up 3 minutes sleepy_bose 82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
``` ```
The following matches containers based on the layer `d0e008c6cf02` or an image The following matches containers based on the layer `d0e008c6cf02` or an image
@@ -256,7 +256,7 @@ that have this layer in its layer stack.
$ docker ps --filter ancestor=d0e008c6cf02 $ docker ps --filter ancestor=d0e008c6cf02
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
82a598284012 ubuntu:26.04 "top" 3 minutes ago Up 3 minutes sleepy_bose 82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
``` ```
#### Create time #### Create time
+19 -19
View File
@@ -66,18 +66,18 @@ docker.io/library/debian:latest
Docker images can consist of multiple layers. In the example above, the image Docker images can consist of multiple layers. In the example above, the image
consists of a single layer; `e756f3fdd6a3`. consists of a single layer; `e756f3fdd6a3`.
Layers can be reused by images. For example, the `debian:trixie` image shares Layers can be reused by images. For example, the `debian:bookworm` image shares
its layer with the `debian:latest`. Pulling the `debian:trixie` image therefore its layer with the `debian:latest`. Pulling the `debian:bookworm` image therefore
only pulls its metadata, but not its layers, because the layer is already present only pulls its metadata, but not its layers, because the layer is already present
locally: locally:
```console ```console
$ docker image pull debian:trixie $ docker image pull debian:bookworm
trixie: Pulling from library/debian bookworm: Pulling from library/debian
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510 Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
Status: Downloaded newer image for debian:trixie Status: Downloaded newer image for debian:bookworm
docker.io/library/debian:trixie docker.io/library/debian:bookworm
``` ```
To see which images are present locally, use the [`docker images`](image_ls.md) To see which images are present locally, use the [`docker images`](image_ls.md)
@@ -87,13 +87,13 @@ command:
$ docker images $ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
debian trixie 4eacea30377a 8 days ago 124MB debian bookworm 4eacea30377a 8 days ago 124MB
debian latest 4eacea30377a 8 days ago 124MB debian latest 4eacea30377a 8 days ago 124MB
``` ```
Docker uses a content-addressable image store, and the image ID is a SHA256 Docker uses a content-addressable image store, and the image ID is a SHA256
digest covering the image's configuration and layers. In the example above, digest covering the image's configuration and layers. In the example above,
`debian:trixie` and `debian:latest` have the same image ID because they are `debian:bookworm` and `debian:latest` have the same image ID because they are
the same image tagged with different names. Because they are the same image, the same image tagged with different names. Because they are the same image,
their layers are stored only once and do not consume extra disk space. their layers are stored only once and do not consume extra disk space.
@@ -106,8 +106,8 @@ refer to [understand images, containers, and storage drivers](https://docs.docke
So far, you've pulled images by their name (and "tag"). Using names and tags is So far, you've pulled images by their name (and "tag"). Using names and tags is
a convenient way to work with images. When using tags, you can `docker pull` an a convenient way to work with images. When using tags, you can `docker pull` an
image again to make sure you have the most up-to-date version of that image. image again to make sure you have the most up-to-date version of that image.
For example, `docker pull ubuntu:26.04` pulls the latest version of the Ubuntu For example, `docker pull ubuntu:24.04` pulls the latest version of the Ubuntu
26.04 image. 24.04 image.
In some cases you don't want images to be updated to newer versions, but prefer In some cases you don't want images to be updated to newer versions, but prefer
to use a fixed version of an image. Docker enables you to pull an image by its to use a fixed version of an image. Docker enables you to pull an image by its
@@ -116,16 +116,16 @@ of an image to pull. Doing so, allows you to "pin" an image to that version,
and guarantee that the image you're using is always the same. and guarantee that the image you're using is always the same.
To know the digest of an image, pull the image first. Let's pull the latest To know the digest of an image, pull the image first. Let's pull the latest
`ubuntu:26.04` image from Docker Hub: `ubuntu:24.04` image from Docker Hub:
```console ```console
$ docker pull ubuntu:26.04 $ docker pull ubuntu:24.04
26.04: Pulling from library/ubuntu 24.04: Pulling from library/ubuntu
125a6e411906: Pull complete 125a6e411906: Pull complete
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Downloaded newer image for ubuntu:26.04 Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:26.04 docker.io/library/ubuntu:24.04
``` ```
Docker prints the digest of the image after the pull has finished. In the example Docker prints the digest of the image after the pull has finished. In the example
@@ -213,11 +213,11 @@ shorthand) to see the images that were pulled. The example below shows all the
```console ```console
$ docker image ls --filter reference=ubuntu $ docker image ls --filter reference=ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 24.04 8a3cdc4d1ad3 3 weeks ago 77.9MB ubuntu 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu noble 8a3cdc4d1ad3 3 weeks ago 77.9MB ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu 26.04 35a88802559d 6 weeks ago 78.1MB ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
ubuntu latest 35a88802559d 6 weeks ago 78.1MB ubuntu latest 35a88802559d 6 weeks ago 78.1MB
ubuntu resolute 35a88802559d 6 weeks ago 78.1MB ubuntu noble 35a88802559d 6 weeks ago 78.1MB
``` ```
### Cancel a pull ### Cancel a pull
+4 -3
View File
@@ -180,12 +180,13 @@ code, and authenticate.
```console ```console
$ docker login $ docker login
Using web-based login. To sign in with credentials on the command line, use 'docker login -u <username>' USING WEB-BASED LOGIN
To sign in with credentials on the command line, use 'docker login -u <username>'
Your one-time device confirmation code is: LNFR-PGCJ Your one-time device confirmation code is: LNFR-PGCJ
Open this URL to authenticate: https://login.docker.com/activate?user_code=LNFR-PGCJ Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate
Press ENTER to open the URL in your browser, or Ctrl+C to cancel. Waiting for authentication in the browser…
``` ```
After entering the code in your browser, you are authenticated to Docker Hub After entering the code in your browser, you are authenticated to Docker Hub
+1 -1
View File
@@ -706,7 +706,7 @@ follows:
| `node.platform.os` | Node operating system | `node.platform.os==windows` | | `node.platform.os` | Node operating system | `node.platform.os==windows` |
| `node.platform.arch` | Node architecture | `node.platform.arch==x86_64` | | `node.platform.arch` | Node architecture | `node.platform.arch==x86_64` |
| `node.labels` | User-defined node labels | `node.labels.security==high` | | `node.labels` | User-defined node labels | `node.labels.security==high` |
| `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-26.04` | | `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-24.04` |
`engine.labels` apply to Docker Engine labels like operating system, drivers, `engine.labels` apply to Docker Engine labels like operating system, drivers,
etc. Swarm administrators add `node.labels` for operational purposes by using etc. Swarm administrators add `node.labels` for operational purposes by using
+2 -2
View File
@@ -339,8 +339,8 @@ $ docker events --filter 'type=network'
$ docker events --filter 'container=container_1' --filter 'container=container_2' $ docker events --filter 'container=container_1' --filter 'container=container_2'
2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:26.04) 2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04) 2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (imager=redis:7.2) 2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (imager=redis:7.2)
2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2) 2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2)
+1467 -2
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -35,7 +35,7 @@ reference to create or run a container based on an image.
An image tag is the image version, which defaults to `latest` when omitted. Use An image tag is the image version, which defaults to `latest` when omitted. Use
the tag to run a container from specific version of an image. For example, to the tag to run a container from specific version of an image. For example, to
run version `26.04` of the `ubuntu` image: `docker run ubuntu:26.04`. run version `24.04` of the `ubuntu` image: `docker run ubuntu:24.04`.
#### Image digests #### Image digests
@@ -406,14 +406,14 @@ We have four ways to set user memory usage:
Examples: Examples:
```console ```console
$ docker run -it ubuntu:26.04 /bin/bash $ docker run -it ubuntu:24.04 /bin/bash
``` ```
We set nothing about memory, this means the processes in the container can use We set nothing about memory, this means the processes in the container can use
as much memory and swap memory as they need. as much memory and swap memory as they need.
```console ```console
$ docker run -it -m 300M --memory-swap -1 ubuntu:26.04 /bin/bash $ docker run -it -m 300M --memory-swap -1 ubuntu:24.04 /bin/bash
``` ```
We set memory limit and disabled swap memory limit, this means the processes in We set memory limit and disabled swap memory limit, this means the processes in
@@ -421,7 +421,7 @@ the container can use 300M memory and as much swap memory as they need (if the
host supports swap memory). host supports swap memory).
```console ```console
$ docker run -it -m 300M ubuntu:26.04 /bin/bash $ docker run -it -m 300M ubuntu:24.04 /bin/bash
``` ```
We set memory limit only, this means the processes in the container can use We set memory limit only, this means the processes in the container can use
@@ -430,7 +430,7 @@ We set memory limit only, this means the processes in the container can use
would be 2*300M, so processes can use 300M swap memory as well. would be 2*300M, so processes can use 300M swap memory as well.
```console ```console
$ docker run -it -m 300M --memory-swap 1G ubuntu:26.04 /bin/bash $ docker run -it -m 300M --memory-swap 1G ubuntu:24.04 /bin/bash
``` ```
We set both memory and swap memory, so the processes in the container can use We set both memory and swap memory, so the processes in the container can use
@@ -456,7 +456,7 @@ The following example limits the memory (`-m`) to 500M and sets the memory
reservation to 200M. reservation to 200M.
```console ```console
$ docker run -it -m 500M --memory-reservation 200M ubuntu:26.04 /bin/bash $ docker run -it -m 500M --memory-reservation 200M ubuntu:24.04 /bin/bash
``` ```
Under this configuration, when the container consumes memory more than 200M and Under this configuration, when the container consumes memory more than 200M and
@@ -466,7 +466,7 @@ memory below 200M.
The following example set memory reservation to 1G without a hard memory limit. The following example set memory reservation to 1G without a hard memory limit.
```console ```console
$ docker run -it --memory-reservation 1G ubuntu:26.04 /bin/bash $ docker run -it --memory-reservation 1G ubuntu:24.04 /bin/bash
``` ```
The container can use as much memory as it needs. The memory reservation setting The container can use as much memory as it needs. The memory reservation setting
@@ -484,13 +484,13 @@ The following example limits the memory to 100M and disables the OOM killer for
this container: this container:
```console ```console
$ docker run -it -m 100M --oom-kill-disable ubuntu:26.04 /bin/bash $ docker run -it -m 100M --oom-kill-disable ubuntu:24.04 /bin/bash
``` ```
The following example, illustrates a dangerous way to use the flag: The following example, illustrates a dangerous way to use the flag:
```console ```console
$ docker run -it --oom-kill-disable ubuntu:26.04 /bin/bash $ docker run -it --oom-kill-disable ubuntu:24.04 /bin/bash
``` ```
The container has unlimited memory which can cause the host to run out memory The container has unlimited memory which can cause the host to run out memory
@@ -510,7 +510,7 @@ between 0 and 100. A value of 0 turns off anonymous page swapping. A value of
For example, you can set: For example, you can set:
```console ```console
$ docker run -it --memory-swappiness=0 ubuntu:26.04 /bin/bash $ docker run -it --memory-swappiness=0 ubuntu:24.04 /bin/bash
``` ```
Setting the `--memory-swappiness` option is helpful when you want to retain the Setting the `--memory-swappiness` option is helpful when you want to retain the
@@ -561,7 +561,7 @@ And usually `--cpu-period` should work with `--cpu-quota`.
Examples: Examples:
```console ```console
$ docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:26.04 /bin/bash $ docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:24.04 /bin/bash
``` ```
If there is 1 CPU, this means the container can get 50% CPU worth of run-time every 50ms. If there is 1 CPU, this means the container can get 50% CPU worth of run-time every 50ms.
@@ -582,13 +582,13 @@ We can set cpus in which to allow execution for containers.
Examples: Examples:
```console ```console
$ docker run -it --cpuset-cpus="1,3" ubuntu:26.04 /bin/bash $ docker run -it --cpuset-cpus="1,3" ubuntu:24.04 /bin/bash
``` ```
This means processes in container can be executed on cpu 1 and cpu 3. This means processes in container can be executed on cpu 1 and cpu 3.
```console ```console
$ docker run -it --cpuset-cpus="0-2" ubuntu:26.04 /bin/bash $ docker run -it --cpuset-cpus="0-2" ubuntu:24.04 /bin/bash
``` ```
This means processes in container can be executed on cpu 0, cpu 1 and cpu 2. This means processes in container can be executed on cpu 0, cpu 1 and cpu 2.
@@ -599,14 +599,14 @@ on NUMA systems.
Examples: Examples:
```console ```console
$ docker run -it --cpuset-mems="1,3" ubuntu:26.04 /bin/bash $ docker run -it --cpuset-mems="1,3" ubuntu:24.04 /bin/bash
``` ```
This example restricts the processes in the container to only use memory from This example restricts the processes in the container to only use memory from
memory nodes 1 and 3. memory nodes 1 and 3.
```console ```console
$ docker run -it --cpuset-mems="0-2" ubuntu:26.04 /bin/bash $ docker run -it --cpuset-mems="0-2" ubuntu:24.04 /bin/bash
``` ```
This example restricts the processes in the container to only use memory from This example restricts the processes in the container to only use memory from
@@ -637,8 +637,8 @@ For example, the commands below create two containers with different blkio
weight: weight:
```console ```console
$ docker run -it --name c1 --blkio-weight 300 ubuntu:26.04 /bin/bash $ docker run -it --name c1 --blkio-weight 300 ubuntu:24.04 /bin/bash
$ docker run -it --name c2 --blkio-weight 600 ubuntu:26.04 /bin/bash $ docker run -it --name c2 --blkio-weight 600 ubuntu:24.04 /bin/bash
``` ```
If you do block IO in the two containers at the same time, by, for example: If you do block IO in the two containers at the same time, by, for example:
@@ -854,11 +854,11 @@ For interacting with the network stack, instead of using `--privileged` they
should use `--cap-add=NET_ADMIN` to modify the network interfaces. should use `--cap-add=NET_ADMIN` to modify the network interfaces.
```console ```console
$ docker run -it --rm ubuntu:26.04 ip link add dummy0 type dummy $ docker run -it --rm ubuntu:24.04 ip link add dummy0 type dummy
RTNETLINK answers: Operation not permitted RTNETLINK answers: Operation not permitted
$ docker run -it --rm --cap-add=NET_ADMIN ubuntu:26.04 ip link add dummy0 type dummy $ docker run -it --rm --cap-add=NET_ADMIN ubuntu:24.04 ip link add dummy0 type dummy
``` ```
To mount a FUSE based filesystem, you need to combine both `--cap-add` and To mount a FUSE based filesystem, you need to combine both `--cap-add` and
-16
View File
@@ -62,22 +62,6 @@ func TestNonexistingHelp(t *testing.T) {
}) })
} }
// TestNonexistingVersion ensures correct behaviour when invoking a
// nonexistent plugin with `--version`.
func TestNonexistingVersion(t *testing.T) {
run, _, cleanup := prepare(t)
defer cleanup()
for _, flag := range []string{"--version", "-v"} {
res := icmd.RunCmd(run("nonexistent", flag))
res.Assert(t, icmd.Expected{
ExitCode: 1,
Out: icmd.None,
})
golden.Assert(t, res.Stderr(), "docker-nonexistent-err.golden")
}
}
// TestRunBad ensures correct behaviour when running an existent but invalid plugin // TestRunBad ensures correct behaviour when running an existent but invalid plugin
func TestRunBad(t *testing.T) { func TestRunBad(t *testing.T) {
run, _, cleanup := prepare(t) run, _, cleanup := prepare(t)
-1
View File
@@ -31,7 +31,6 @@ services:
dockerfile: engine/Dockerfile dockerfile: engine/Dockerfile
args: args:
ENGINE_VERSION: ${ENGINE_VERSION:-29} ENGINE_VERSION: ${ENGINE_VERSION:-29}
ENGINE_SOURCE: ${ENGINE_SOURCE:-dind}
privileged: true privileged: true
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental'] command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
environment: environment:
+1 -1
View File
@@ -112,7 +112,7 @@ func TestRunWithCgroupNamespace(t *testing.T) {
func TestRunUmask(t *testing.T) { func TestRunUmask(t *testing.T) {
environment.SkipIfDaemonNotLinux(t) environment.SkipIfDaemonNotLinux(t)
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.56")) t.Skip("FIXME: un-skip once e2e tests v29.8.0")
testCases := []struct { testCases := []struct {
name string name string
+1 -1
View File
@@ -30,5 +30,5 @@ func TestOauthLogin(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
output, _ := io.ReadAll(p) output, _ := io.ReadAll(p)
assert.Check(t, strings.Contains(string(output), "Using web-based login"), string(output)) assert.Check(t, strings.Contains(string(output), "USING WEB-BASED LOGIN"), string(output))
} }
+1 -9
View File
@@ -3,16 +3,8 @@
# ENGINE_VERSION is the version of the (docker-in-docker) Docker Engine to # ENGINE_VERSION is the version of the (docker-in-docker) Docker Engine to
# test against. # test against.
ARG ENGINE_VERSION=29 ARG ENGINE_VERSION=29
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master).
ARG ENGINE_SOURCE=dind
FROM docker:${ENGINE_VERSION}-dind AS engine-dind FROM docker:${ENGINE_VERSION}-dind
# moby/moby-bin only has the daemon binaries.
FROM docker:dind AS engine-moby-bin
COPY --link --from=moby/moby-bin:master / /usr/local/bin/
FROM engine-${ENGINE_SOURCE}
# the openssh-client update is needed for security reasons when using docker:23.0-dind, currently maintained as an lts by mirantis # the openssh-client update is needed for security reasons when using docker:23.0-dind, currently maintained as an lts by mirantis
RUN apk --no-cache add openssl openssh-client openssh-server shadow && \ RUN apk --no-cache add openssl openssh-client openssh-server shadow && \
+1 -10
View File
@@ -1,14 +1,5 @@
ARG ENGINE_VERSION ARG ENGINE_VERSION
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master). FROM docker:${ENGINE_VERSION}-dind
ARG ENGINE_SOURCE=dind
FROM docker:${ENGINE_VERSION}-dind AS engine-dind
# moby/moby-bin only has the daemon binaries.
FROM docker:dind AS engine-moby-bin
COPY --link --from=moby/moby-bin:master / /usr/local/bin/
FROM engine-${ENGINE_SOURCE}
# Trust the tlsregistry CA so dockerd connects without --insecure-registry. # Trust the tlsregistry CA so dockerd connects without --insecure-registry.
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
+14 -27
View File
@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"strings" "strings"
"github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/credentials"
@@ -18,6 +17,8 @@ import (
"github.com/docker/cli/internal/registry" "github.com/docker/cli/internal/registry"
"github.com/docker/cli/internal/tui" "github.com/docker/cli/internal/tui"
"github.com/morikuni/aec" "github.com/morikuni/aec"
"github.com/sirupsen/logrus"
"github.com/pkg/browser" "github.com/pkg/browser"
) )
@@ -82,18 +83,19 @@ var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login
// The retrieved tokens are stored in the credentials store (under a separate // The retrieved tokens are stored in the credentials store (under a separate
// key), and the refresh token is concatenated with the client ID. // key), and the refresh token is concatenated with the client ID.
func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.AuthConfig, error) { func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.AuthConfig, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
state, err := m.api.GetDeviceCode(ctx, m.audience) state, err := m.api.GetDeviceCode(ctx, m.audience)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: %w", ErrDeviceLoginStartFail, err) logrus.Debugf("failed to start device code login: %v", err)
return nil, ErrDeviceLoginStartFail
} }
if state.UserCode == "" { if state.UserCode == "" {
return nil, fmt.Errorf("%w: missing user code", ErrDeviceLoginStartFail) logrus.Debugf("failed to start device code login: missing user code")
return nil, ErrDeviceLoginStartFail
} }
_, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB-BASED LOGIN"))
var out tui.Output var out tui.Output
switch stream := w.(type) { switch stream := w.(type) {
case *streams.Out: case *streams.Out:
@@ -101,10 +103,9 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut
default: default:
out = tui.NewOutput(streams.NewOut(w)) out = tui.NewOutput(streams.NewOut(w))
} }
out.PrintNote("Using web-based login. To sign in with credentials on the command line, use 'docker login -u <username>'\n") out.PrintNote("To sign in with credentials on the command line, use 'docker login -u <username>'\n")
_, _ = fmt.Fprintln(w, "Your one-time device confirmation code is:", aec.Bold.Apply(state.UserCode)) _, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode)
_, _ = fmt.Fprintln(w, "Open this URL to authenticate:", aec.Underline.Apply(state.VerificationURI)) _, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0])
_, _ = fmt.Fprintln(w, "\n"+aec.Bold.Apply("Press ENTER")+" to open the URL in your browser, or "+aec.Bold.Apply("Ctrl+C")+" to cancel.")
tokenResChan := make(chan api.TokenResponse) tokenResChan := make(chan api.TokenResponse)
waitForTokenErrChan := make(chan error) waitForTokenErrChan := make(chan error)
@@ -119,25 +120,11 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut
go func() { go func() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
if _, err := reader.ReadString('\n'); err != nil || ctx.Err() != nil { _, _ = reader.ReadString('\n')
return _ = m.openBrowser(state.VerificationURI)
}
err := m.openBrowser(state.VerificationURI)
if ctx.Err() != nil {
return
}
if err != nil {
if errors.Is(err, exec.ErrNotFound) {
_, _ = fmt.Fprintln(w, "No browser found. Open the URL above manually to continue.")
} else {
_, _ = fmt.Fprintln(w, "Failed to open browser:", err)
_, _ = fmt.Fprintln(w, "Open the URL above manually to continue.")
}
return
}
_, _ = fmt.Fprintln(w, "\nOpening browser…")
}() }()
_, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n")
var tokenRes api.TokenResponse var tokenRes api.TokenResponse
select { select {
case <-ctx.Done(): case <-ctx.Done():
+2 -2
View File
@@ -69,10 +69,10 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
// get the http transport, this will be used in a client to upload manifest // get the http transport, this will be used in a client to upload manifest
base := &http.Transport{ base := &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{ Dial: (&net.Dialer{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
}).DialContext, }).Dial,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: endpoint.TLSConfig, TLSClientConfig: endpoint.TLSConfig,
DisableKeepAlives: true, DisableKeepAlives: true,
+3 -6
View File
@@ -27,13 +27,8 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
})) }))
cli.SetOut(outStream) cli.SetOut(outStream)
r, w, err := os.Pipe() r, _, err := os.Pipe()
assert.NilError(t, err) assert.NilError(t, err)
t.Cleanup(func() {
_ = w.Close()
_ = r.Close()
})
cli.SetIn(streams.NewIn(r)) cli.SetIn(streams.NewIn(r))
notifyCtx, notifyCancel := context.WithCancel(ctx) notifyCtx, notifyCancel := context.WithCancel(ctx)
@@ -63,6 +58,8 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
}() }()
} }
assert.Check(t, cli.OutBuffer().Len() > 0)
// a small delay to ensure the plugin is prompting // a small delay to ensure the plugin is prompting
time.Sleep(100 * time.Microsecond) time.Sleep(100 * time.Microsecond)
+6 -14
View File
@@ -83,24 +83,16 @@ set as the **URL**, the repository is cloned locally and then sent as the contex
Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip. Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip.
The **--add-host** option can be set multiple times. The **--add-host** option can be set multiple times.
**--build-arg** *name*[=*value*] **--build-arg** *variable*
Set a build-time variable. The *name* is the variable name, as declared name and value of a **buildarg**.
by an `ARG` instruction in the Dockerfile. If *value* is omitted, the
value of the variable *name* from the local environment is used; if it is
not set there either, the default from the `ARG` instruction (if any) is
used.
Build-time variables are available as environment variables to commands
run by the Dockerfile's `RUN` instructions, and can be used for variable
expansion in other Dockerfile instructions. An environment variable defined
by an `ENV` instruction always overrides an `ARG` of the same name. This
mechanism is not meant for passing secret values.
For example, if you want to pass a value for `http_proxy`, use For example, if you want to pass a value for `http_proxy`, use
`--build-arg=http_proxy="http://some.proxy.url"` `--build-arg=http_proxy="http://some.proxy.url"`
See also https://docs.docker.com/reference/dockerfile/#arg and Users pass these values at build-time. Docker uses the `buildargs` as the
https://docs.docker.com/reference/cli/docker/buildx/build/#build-arg. environment context for command(s) run via the Dockerfile's `RUN` instruction
or for variable expansion in other Dockerfile instructions. This is not meant
for passing secret values. [Read more about the buildargs instruction](https://docs.docker.com/engine/reference/builder/#arg)
**--cache-from** "" **--cache-from** ""
Set image that will be used as a build cache source. Set image that will be used as a build cache source.
+2 -2
View File
@@ -48,10 +48,10 @@ Valid placeholders for the Go template are listed below:
$ docker container ls -a $ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a87ecb4f327c ubuntu:26.04 /bin/sh -c #(nop) MA 20 minutes ago Exit 0 desperate_brattain a87ecb4f327c ubuntu:24.04 /bin/sh -c #(nop) MA 20 minutes ago Exit 0 desperate_brattain
01946d9d34d8 busybox /bin/sh -c #(nop) MA 33 minutes ago Exit 0 thirsty_bell 01946d9d34d8 busybox /bin/sh -c #(nop) MA 33 minutes ago Exit 0 thirsty_bell
c1d3b0166030 acffc0358b9e /bin/sh -c yum -y up 2 weeks ago Exit 1 determined_torvalds c1d3b0166030 acffc0358b9e /bin/sh -c yum -y up 2 weeks ago Exit 1 determined_torvalds
41d50ecd2f57 ubuntu:26.04 /bin/sh -c #(nop) MA 2 weeks ago Exit 0 drunk_pike 41d50ecd2f57 ubuntu:24.04 /bin/sh -c #(nop) MA 2 weeks ago Exit 0 drunk_pike
## Display only IDs of all containers, including non-running ## Display only IDs of all containers, including non-running
+1 -1
View File
@@ -10,7 +10,7 @@ The title REPOSITORY for the first title may seem confusing. It is essentially
the image name. However, because you can tag a specific image, and multiple tags the image name. However, because you can tag a specific image, and multiple tags
(image instances) can be associated with a single name, the name is really a (image instances) can be associated with a single name, the name is really a
repository for all tagged images of the same name. For example consider an image repository for all tagged images of the same name. For example consider an image
called ubuntu. It may be tagged with 26.04 or 24.04, etc. to manage different called ubuntu. It may be tagged with 24.04 or 22.04, etc. to manage different
versions. versions.
## Filters ## Filters
+19 -19
View File
@@ -27,17 +27,17 @@ Docker Engine uses the `:latest` tag as a default. This example pulls the
Docker images can consist of multiple layers. In the example above, the image Docker images can consist of multiple layers. In the example above, the image
consists of a single layer; `e756f3fdd6a3`. consists of a single layer; `e756f3fdd6a3`.
Layers can be reused by images. For example, the `debian:trixie` image shares Layers can be reused by images. For example, the `debian:bookworm` image shares
its layer with the `debian:latest`. Pulling the `debian:trixie` image therefore its layer with the `debian:latest`. Pulling the `debian:bookworm` image therefore
only pulls its metadata, but not its layers, because the layer is already present only pulls its metadata, but not its layers, because the layer is already present
locally: locally:
$ docker image pull debian:trixie $ docker image pull debian:bookworm
trixie: Pulling from library/debian bookworm: Pulling from library/debian
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510 Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
Status: Downloaded newer image for debian:trixie Status: Downloaded newer image for debian:bookworm
docker.io/library/debian:trixie docker.io/library/debian:bookworm
To see which images are present locally, use the **docker-images(1)** To see which images are present locally, use the **docker-images(1)**
command: command:
@@ -45,12 +45,12 @@ command:
$ docker images $ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
debian trixie 4eacea30377a 8 days ago 124MB debian bookworm 4eacea30377a 8 days ago 124MB
debian latest 4eacea30377a 8 days ago 124MB debian latest 4eacea30377a 8 days ago 124MB
Docker uses a content-addressable image store, and the image ID is a SHA256 Docker uses a content-addressable image store, and the image ID is a SHA256
digest covering the image's configuration and layers. In the example above, digest covering the image's configuration and layers. In the example above,
`debian:trixie` and `debian:latest` have the same image ID because they are `debian:bookworm` and `debian:latest` have the same image ID because they are
the *same* image tagged with different names. Because they are the same image, the *same* image tagged with different names. Because they are the same image,
their layers are stored only once and do not consume extra disk space. their layers are stored only once and do not consume extra disk space.
@@ -64,8 +64,8 @@ in the online documentation.
So far, you've pulled images by their name (and "tag"). Using names and tags is So far, you've pulled images by their name (and "tag"). Using names and tags is
a convenient way to work with images. When using tags, you can `docker image pull` an a convenient way to work with images. When using tags, you can `docker image pull` an
image again to make sure you have the most up-to-date version of that image. image again to make sure you have the most up-to-date version of that image.
For example, `docker image pull ubuntu:26.04` pulls the latest version of the Ubuntu For example, `docker image pull ubuntu:24.04` pulls the latest version of the Ubuntu
26.04 image. 24.04 image.
In some cases you don't want images to be updated to newer versions, but prefer In some cases you don't want images to be updated to newer versions, but prefer
to use a fixed version of an image. Docker enables you to pull an image by its to use a fixed version of an image. Docker enables you to pull an image by its
@@ -74,15 +74,15 @@ of an image to pull. Doing so, allows you to "pin" an image to that version,
and guarantee that the image you're using is always the same. and guarantee that the image you're using is always the same.
To know the digest of an image, pull the image first. Let's pull the latest To know the digest of an image, pull the image first. Let's pull the latest
`ubuntu:26.04` image from Docker Hub: `ubuntu:24.04` image from Docker Hub:
$ docker image pull ubuntu:26.04 $ docker image pull ubuntu:24.04
26.04: Pulling from library/ubuntu 24.04: Pulling from library/ubuntu
125a6e411906: Pull complete 125a6e411906: Pull complete
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Downloaded newer image for ubuntu:26.04 Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:26.04 docker.io/library/ubuntu:24.04
Docker prints the digest of the image after the pull has finished. In the example Docker prints the digest of the image after the pull has finished. In the example
above, the digest of the image is: above, the digest of the image is:
@@ -159,11 +159,11 @@ images that are present locally:
$ docker image ls --filter reference=ubuntu $ docker image ls --filter reference=ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 24.04 8a3cdc4d1ad3 3 weeks ago 77.9MB ubuntu 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu noble 8a3cdc4d1ad3 3 weeks ago 77.9MB ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu 26.04 35a88802559d 6 weeks ago 78.1MB ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
ubuntu latest 35a88802559d 6 weeks ago 78.1MB ubuntu latest 35a88802559d 6 weeks ago 78.1MB
ubuntu resolute 35a88802559d 6 weeks ago 78.1MB ubuntu noble 35a88802559d 6 weeks ago 78.1MB
## Cancel a pull ## Cancel a pull
+9 -9
View File
@@ -62,8 +62,8 @@ The following example outputs all events that were generated in the last 3 minut
relative to the current time on the client machine: relative to the current time on the client machine:
# docker events --since '3m' # docker events --since '3m'
2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu:26.04) die 2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu:24.04) die
2015-05-12T15:52:12.999999999Z07:00 4386fb97867d: (from ubuntu:26.04) stop 2015-05-12T15:52:12.999999999Z07:00 4386fb97867d: (from ubuntu:24.04) stop
2015-05-12T15:53:45.999999999Z07:00 7805c1d35632: (from redis:7.2) die 2015-05-12T15:53:45.999999999Z07:00 7805c1d35632: (from redis:7.2) die
2015-05-12T15:54:03.999999999Z07:00 7805c1d35632: (from redis:7.2) stop 2015-05-12T15:54:03.999999999Z07:00 7805c1d35632: (from redis:7.2) stop
@@ -97,21 +97,21 @@ Lines. For information about JSON Lines, refer to https://jsonlines.org .
## Filters ## Filters
$ docker events --filter 'event=stop' $ docker events --filter 'event=stop'
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04) 2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
2014-09-03T17:42:14.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2) 2014-09-03T17:42:14.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2)
$ docker events --filter 'image=ubuntu:26.04' $ docker events --filter 'image=ubuntu:24.04'
2014-05-10T17:42:14.999999999Z07:00 container start 4386fb97867d (image=ubuntu:26.04) 2014-05-10T17:42:14.999999999Z07:00 container start 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container die 4386fb97867d (image=ubuntu:26.04) 2014-05-10T17:42:14.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04) 2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
$ docker events --filter 'container=7805c1d35632' $ docker events --filter 'container=7805c1d35632'
2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:7.2) 2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:7.2)
2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image= redis:7.2) 2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image= redis:7.2)
$ docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d' $ docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d'
2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:26.04) 2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04) 2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:7.2) 2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:7.2)
2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2) 2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2)
+2 -2
View File
@@ -71,8 +71,8 @@ Server:
seccomp seccomp
Profile: builtin Profile: builtin
cgroupns cgroupns
Kernel Version: 6.8.0-31-generic Kernel Version: 5.15.0-25-generic
Operating System: Ubuntu 24.04 LTS Operating System: Ubuntu 22.04 LTS
OSType: linux OSType: linux
Architecture: x86_64 Architecture: x86_64
CPUs: 1 CPUs: 1
+11 -3
View File
@@ -33,6 +33,8 @@ func parseCount(s string) (int, error) {
} }
// Set a new mount value // Set a new mount value
//
//nolint:gocyclo
func (o *GpuOpts) Set(value string) error { func (o *GpuOpts) Set(value string) error {
csvReader := csv.NewReader(strings.NewReader(value)) csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read() fields, err := csvReader.Read()
@@ -46,14 +48,20 @@ func (o *GpuOpts) Set(value string) error {
// Set writable as the default // Set writable as the default
for _, field := range fields { for _, field := range fields {
key, val, withValue := strings.Cut(field, "=") key, val, withValue := strings.Cut(field, "=")
if !withValue {
key, val = "count", key
}
if _, ok := seen[key]; ok { if _, ok := seen[key]; ok {
return fmt.Errorf("gpu request key '%s' can be specified only once", key) return fmt.Errorf("gpu request key '%s' can be specified only once", key)
} }
seen[key] = struct{}{} seen[key] = struct{}{}
if !withValue {
seen["count"] = struct{}{}
req.Count, err = parseCount(key)
if err != nil {
return err
}
continue
}
switch key { switch key {
case "driver": case "driver":
req.Driver = val req.Driver = val

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