mirror of
https://github.com/docker/cli.git
synced 2026-11-13 08:13:40 -04:00
Compare commits
107
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaae503a22 | ||
|
|
25552ba236 | ||
|
|
6eb5faf90a | ||
|
|
e439fbef16 | ||
|
|
17b57bb493 | ||
|
|
71758c77cc | ||
|
|
d4906a8547 | ||
|
|
5587534468 | ||
|
|
06e6e1db50 | ||
|
|
0dfbfa6de0 | ||
|
|
2f478a083d | ||
|
|
e5ef05054c | ||
|
|
c55feebf74 | ||
|
|
83d2f9c986 | ||
|
|
bc9b8d9a15 | ||
|
|
6758ba62d0 | ||
|
|
1894f9a25a | ||
|
|
258c9ea25e | ||
|
|
2cc835a842 | ||
|
|
e9fa001901 | ||
|
|
3399deb9e7 | ||
|
|
5dce5cf00a | ||
|
|
26f11944b9 | ||
|
|
2914f034fa | ||
|
|
a5d7547c39 | ||
|
|
532b10a36f | ||
|
|
7c0f1b261a | ||
|
|
e863f84b15 | ||
|
|
e969249cf5 | ||
|
|
bad50f6bbe | ||
|
|
89e99a7ee4 | ||
|
|
e63c04ae28 | ||
|
|
c3a866740e | ||
|
|
dfe5bf3073 | ||
|
|
85af295d30 | ||
|
|
de692998a4 | ||
|
|
873db34f4b | ||
|
|
66da404208 | ||
|
|
a0f7df60ba | ||
|
|
8fa64e724a | ||
|
|
e2aefc6e1a | ||
|
|
26a4786788 | ||
|
|
8f1203743d | ||
|
|
32a838bf17 | ||
|
|
cbb0dabae0 | ||
|
|
d216e46795 | ||
|
|
1c6c7ffcc8 | ||
|
|
11626f30d0 | ||
|
|
7817ee1c5c | ||
|
|
6b88c47537 | ||
|
|
2b3688febb | ||
|
|
28f460abf3 | ||
|
|
1f6f73a7be | ||
|
|
f9ad558f5d | ||
|
|
dd422f424c | ||
|
|
965590b378 | ||
|
|
191aab9fe8 | ||
|
|
682373a887 | ||
|
|
949b412ae3 | ||
|
|
57393ea18e | ||
|
|
84e55d46d7 | ||
|
|
18958e4c02 | ||
|
|
ab54f1f097 | ||
|
|
d278c1ec68 | ||
|
|
fb2fe0eac7 | ||
|
|
920919bd15 | ||
|
|
beafae2696 | ||
|
|
b9456fd725 | ||
|
|
7a75f75b96 | ||
|
|
9762c1688f | ||
|
|
b2f5d905c7 | ||
|
|
77ebef3df8 | ||
|
|
6d15cedd97 | ||
|
|
82b833e57a | ||
|
|
997674f459 | ||
|
|
81e4a109a3 | ||
|
|
e7ece565b2 | ||
|
|
4214721b3b | ||
|
|
281291299e | ||
|
|
796f9633b4 | ||
|
|
29b22240bc | ||
|
|
19a353f2ea | ||
|
|
3bc38f9c19 | ||
|
|
0ccbdf86a0 | ||
|
|
fbafb4965c | ||
|
|
276829e744 | ||
|
|
cc8c87844d | ||
|
|
e512522843 | ||
|
|
8c8a77521a | ||
|
|
bf8066ca18 | ||
|
|
d35139c89f | ||
|
|
b32aa42608 | ||
|
|
8ff6f243a2 | ||
|
|
b9312b9d88 | ||
|
|
ccccec73b0 | ||
|
|
dd8edb6863 | ||
|
|
66786b775a | ||
|
|
47e9dbf7b0 | ||
|
|
abf2f34c74 | ||
|
|
dd50814004 | ||
|
|
6998c79cfd | ||
|
|
c7bdacad70 | ||
|
|
1faf23e25f | ||
|
|
b8dd18fe2c | ||
|
|
a3539bd5c9 | ||
|
|
adfb114576 | ||
|
|
a999520f67 |
@@ -68,7 +68,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
set: |
|
set: |
|
||||||
@@ -99,7 +99,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@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0
|
uses: docker/github-builder/.github/workflows/bake.yml@a31d2311da4d55e21b978c1c52bab82ba97d51ee # v1.19.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
|
||||||
@@ -159,7 +159,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||||
with:
|
with:
|
||||||
targets: plugins-cross
|
targets: plugins-cross
|
||||||
set: |
|
set: |
|
||||||
|
|||||||
@@ -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.26.10"
|
go-version: "1.27.4"
|
||||||
cache: false
|
cache: false
|
||||||
-
|
-
|
||||||
name: Initialize CodeQL
|
name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
uses: github/codeql-action/init@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3
|
||||||
with:
|
with:
|
||||||
languages: go
|
languages: go
|
||||||
-
|
-
|
||||||
name: Autobuild
|
name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
uses: github/codeql-action/autobuild@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3
|
||||||
-
|
-
|
||||||
name: Perform CodeQL Analysis
|
name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
uses: github/codeql-action/analyze@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3
|
||||||
with:
|
with:
|
||||||
category: "/language:go"
|
category: "/language:go"
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ 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
|
||||||
@@ -80,4 +83,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
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
use_oidc: true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
review:
|
review:
|
||||||
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@3654ec391b070e247de2e2e3c1ff2519b6378b3a # v2.0.9
|
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@aaa657c53cc69ed15683cd8a985d86663a0e1594 # v2.1.0
|
||||||
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
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ 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:
|
||||||
-
|
-
|
||||||
@@ -33,7 +36,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||||
with:
|
with:
|
||||||
targets: test-coverage
|
targets: test-coverage
|
||||||
-
|
-
|
||||||
@@ -41,9 +44,12 @@ 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
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
use_oidc: true
|
||||||
|
|
||||||
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 }}
|
||||||
@@ -53,9 +59,9 @@ 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:
|
||||||
-
|
-
|
||||||
@@ -68,7 +74,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.26.10"
|
go-version: "1.27.4"
|
||||||
cache: false
|
cache: false
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
@@ -86,4 +92,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
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
use_oidc: true
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Run
|
name: Run
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
@@ -101,7 +101,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.26.10"
|
go-version: "1.27.4"
|
||||||
cache: false
|
cache: false
|
||||||
-
|
-
|
||||||
name: Run gocompat check
|
name: Run gocompat check
|
||||||
|
|||||||
+1
-11
@@ -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.26.10"
|
go: "1.27.4"
|
||||||
|
|
||||||
timeout: 5m
|
timeout: 5m
|
||||||
|
|
||||||
@@ -201,11 +201,6 @@ 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:
|
||||||
@@ -227,11 +222,6 @@ 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
|
||||||
|
|||||||
+8
-6
@@ -5,10 +5,12 @@ 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.23
|
ARG ALPINE_VERSION=3.24
|
||||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
|
||||||
|
|
||||||
ARG GO_VERSION=1.26.10
|
# BASE_DEBIAN_DISTRO sets the version of the debian base image to use,
|
||||||
|
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.
|
||||||
@@ -25,12 +27,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.0
|
ARG BUILDX_VERSION=0.38.2
|
||||||
|
|
||||||
# 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.6.0
|
ARG COMPOSE_VERSION=v5.7.0
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||||
|
|
||||||
@@ -53,7 +55,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-12-dev pkgconf
|
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-14-dev pkgconf
|
||||||
|
|
||||||
FROM build-base-${BASE_VARIANT} AS goversioninfo
|
FROM build-base-${BASE_VARIANT} AS goversioninfo
|
||||||
ARG GOVERSIONINFO_VERSION
|
ARG GOVERSIONINFO_VERSION
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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"
|
||||||
@@ -32,6 +33,11 @@ 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)
|
||||||
@@ -69,6 +75,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -83,6 +90,59 @@ 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
|
||||||
@@ -212,8 +272,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
|
// needlessly initializes the client and tries to connect to the
|
||||||
// connect to the daemon.
|
// 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)
|
||||||
|
|||||||
+19
-1
@@ -160,7 +160,25 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro
|
|||||||
return nil, nil, cmd.FlagErrorFunc()(cmd, err)
|
return nil, nil, cmd.FlagErrorFunc()(cmd, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd, flags.Args(), nil
|
args := flags.Args()
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|||||||
@@ -108,3 +108,45 @@ 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
configtypes "github.com/docker/cli/cli/config/types"
|
|
||||||
dcontext "github.com/docker/cli/cli/context"
|
dcontext "github.com/docker/cli/cli/context"
|
||||||
"github.com/docker/cli/cli/context/docker"
|
"github.com/docker/cli/cli/context/docker"
|
||||||
"github.com/docker/cli/cli/context/store"
|
"github.com/docker/cli/cli/context/store"
|
||||||
@@ -24,7 +23,6 @@ import (
|
|||||||
cliflags "github.com/docker/cli/cli/flags"
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/cli/version"
|
"github.com/docker/cli/cli/version"
|
||||||
"github.com/docker/cli/internal/secretsengine"
|
|
||||||
dopts "github.com/docker/cli/opts"
|
dopts "github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/build"
|
"github.com/moby/moby/api/types/build"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
@@ -77,8 +75,6 @@ type DockerCli struct {
|
|||||||
contextStoreConfig *store.Config
|
contextStoreConfig *store.Config
|
||||||
initTimeout time.Duration
|
initTimeout time.Duration
|
||||||
res telemetryResource
|
res telemetryResource
|
||||||
registryAuthOnce sync.Once
|
|
||||||
registryAuth secretsengine.AuthProvider
|
|
||||||
|
|
||||||
// baseCtx is the base context used for internal operations. In the future
|
// baseCtx is the base context used for internal operations. In the future
|
||||||
// this may be replaced by explicitly passing a context to functions that
|
// this may be replaced by explicitly passing a context to functions that
|
||||||
@@ -145,21 +141,6 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
|
|||||||
return cli.configFile
|
return cli.configFile
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *DockerCli) RegistryAuthProvider() secretsengine.AuthProvider {
|
|
||||||
cli.registryAuthOnce.Do(func() {
|
|
||||||
cli.registryAuth = secretsengine.NewDesktop(configFileAuth{cli})
|
|
||||||
})
|
|
||||||
return cli.registryAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
type configFileAuth struct {
|
|
||||||
cli *DockerCli
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c configFileAuth) GetAuthConfig(registryHostname string) (configtypes.AuthConfig, error) {
|
|
||||||
return c.cli.ConfigFile().GetAuthConfig(registryHostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerInfo returns the server version details for the host this client is
|
// ServerInfo returns the server version details for the host this client is
|
||||||
// connected to
|
// connected to
|
||||||
func (cli *DockerCli) ServerInfo() ServerInfo {
|
func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
configtypes "github.com/docker/cli/cli/config/types"
|
|
||||||
"github.com/docker/cli/cli/context/store"
|
"github.com/docker/cli/cli/context/store"
|
||||||
"github.com/docker/cli/cli/flags"
|
"github.com/docker/cli/cli/flags"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
@@ -502,17 +501,3 @@ func TestNewDockerCliWithAPIClientOptions(t *testing.T) {
|
|||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegistryAuthProvider(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
cli := &DockerCli{configFile: &configfile.ConfigFile{}}
|
|
||||||
p := cli.RegistryAuthProvider()
|
|
||||||
assert.Assert(t, p == cli.RegistryAuthProvider(), "the provider must be created once per DockerCli")
|
|
||||||
|
|
||||||
cli.configFile = &configfile.ConfigFile{AuthConfigs: map[string]configtypes.AuthConfig{
|
|
||||||
"ghcr.io": {Username: "user", Password: "pass"},
|
|
||||||
}}
|
|
||||||
got, err := p.GetAuthConfig("ghcr.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, configtypes.AuthConfig{Username: "user", Password: "pass"})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -335,23 +335,27 @@ 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)
|
||||||
res := archive.CopyTo(preArchive, srcInfo, dstPath)
|
err = 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 res
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -474,11 +478,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -446,3 +447,64 @@ 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())
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/docker/cli/cli/config/types"
|
"github.com/docker/cli/cli/config/types"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/cli/internal/registryauth"
|
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/mount"
|
"github.com/moby/moby/api/types/mount"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
@@ -134,7 +133,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pullImage(ctx context.Context, dockerCLI command.Cli, img string, options *createOptions) error {
|
func pullImage(ctx context.Context, dockerCLI command.Cli, img string, options *createOptions) error {
|
||||||
encodedAuth, err := registryauth.EncodedAuth(dockerCLI, img)
|
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,24 +146,27 @@ 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 {
|
||||||
var b strings.Builder
|
|
||||||
for i, n := range c.c.Names {
|
|
||||||
name := strings.TrimPrefix(n, "/")
|
|
||||||
if c.trunc {
|
if c.trunc {
|
||||||
// When printing truncated, we only print a single name.
|
// When printing truncated, we only print a single name.
|
||||||
//
|
//
|
||||||
// Pick the first name that's not a legacy link (does not have
|
// Pick the first name that's not a legacy link (does not have
|
||||||
// slashes inside the name itself (e.g., "/other-container/link")).
|
// slashes inside the name itself (e.g., "/other-container/link")).
|
||||||
// Normally this would be the first name found.
|
// Normally this would be the first name found.
|
||||||
if strings.IndexByte(name, '/') == -1 {
|
for _, n := range c.c.Names {
|
||||||
|
if name := strings.TrimPrefix(n, "/"); strings.IndexByte(name, '/') == -1 {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
// 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
|
||||||
|
for i, n := range c.c.Names {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
b.WriteByte(',')
|
b.WriteByte(',')
|
||||||
}
|
}
|
||||||
b.WriteString(name)
|
b.WriteString(strings.TrimPrefix(n, "/"))
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,25 @@ 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,
|
||||||
@@ -257,6 +276,7 @@ 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()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/cli/internal/registryauth"
|
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -91,7 +90,7 @@ func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error
|
|||||||
ociPlatforms = append(ociPlatforms, p)
|
ociPlatforms = append(ociPlatforms, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
encodedAuth, err := registryauth.EncodedAuth(dockerCLI, distributionRef.String())
|
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), distributionRef.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/cli/internal/registryauth"
|
|
||||||
"github.com/docker/cli/internal/tui"
|
"github.com/docker/cli/internal/tui"
|
||||||
"github.com/moby/moby/api/types/auxprogress"
|
"github.com/moby/moby/api/types/auxprogress"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
@@ -109,7 +108,7 @@ To push the complete multi-platform image, remove the --platform flag.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the Auth config relevant for this server
|
// Resolve the Auth config relevant for this server
|
||||||
encodedAuth, err := registryauth.EncodedAuth(dockerCli, ref.String())
|
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), ref.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ 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"
|
||||||
@@ -20,6 +22,23 @@ 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
|
||||||
@@ -97,6 +116,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,8 +160,15 @@ func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error {
|
|||||||
imageManifest.Descriptor.Platform.OSVersion = opts.osVersion
|
imageManifest.Descriptor.Platform.OSVersion = opts.osVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isValidOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture) {
|
// Validate platform syntax, without restricting OS/architecture combinations.
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,15 +58,16 @@ 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"})
|
||||||
cmd.Flags().Set("os", "freebsd")
|
assert.NilError(t, cmd.Flags().Set("os", "freebsd"))
|
||||||
cmd.Flags().Set("arch", "fake")
|
assert.NilError(t, cmd.Flags().Set("arch", "<invalid>"))
|
||||||
cmd.Flags().Set("os-version", "1")
|
assert.NilError(t, cmd.Flags().Set("os-version", "1"))
|
||||||
cmd.Flags().Set("os-features", "feature1")
|
assert.NilError(t, cmd.Flags().Set("os-features", "feature1"))
|
||||||
cmd.Flags().Set("variant", "v7")
|
assert.NilError(t, 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)
|
||||||
|
|
||||||
cmd.Flags().Set("arch", "arm")
|
assert.NilError(t, cmd.Flags().Set("arch", "arm"))
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
|
|
||||||
cmd = newInspectCommand(cli)
|
cmd = newInspectCommand(cli)
|
||||||
|
|||||||
@@ -9,53 +9,6 @@ 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 {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/cli/internal/prompt"
|
"github.com/docker/cli/internal/prompt"
|
||||||
"github.com/docker/cli/internal/registryauth"
|
|
||||||
"github.com/moby/moby/api/types/plugin"
|
"github.com/moby/moby/api/types/plugin"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -61,7 +60,7 @@ func buildPullConfig(dockerCLI command.Cli, opts pluginOptions) (client.PluginIn
|
|||||||
return client.PluginInstallOptions{}, err
|
return client.PluginInstallOptions{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
encodedAuth, err := registryauth.EncodedAuth(dockerCLI, ref.String())
|
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), ref.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return client.PluginInstallOptions{}, err
|
return client.PluginInstallOptions{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"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/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/cli/internal/registryauth"
|
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -43,7 +42,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
named = reference.TagNameOnly(named)
|
named = reference.TagNameOnly(named)
|
||||||
encodedAuth, err := registryauth.EncodedAuth(dockerCli, named.String())
|
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), named.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"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/command/completion"
|
||||||
"github.com/docker/cli/internal/registryauth"
|
|
||||||
cliopts "github.com/docker/cli/opts"
|
cliopts "github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/moby/moby/api/types/swarm"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
@@ -121,7 +120,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
|||||||
if opts.registryAuth {
|
if opts.registryAuth {
|
||||||
// Retrieve encoded auth token from the image reference
|
// Retrieve encoded auth token from the image reference
|
||||||
var err error
|
var err error
|
||||||
encodedAuth, err = registryauth.EncodedAuth(dockerCLI, opts.image)
|
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), opts.image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"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/command/completion"
|
||||||
"github.com/docker/cli/internal/registryauth"
|
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/docker/cli/opts/swarmopts"
|
"github.com/docker/cli/opts/swarmopts"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/moby/moby/api/types/container"
|
||||||
@@ -224,7 +223,7 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
|||||||
// Retrieve encoded auth token from the image reference
|
// Retrieve encoded auth token from the image reference
|
||||||
// This would be the old image if it didn't change in this update
|
// This would be the old image if it didn't change in this update
|
||||||
var err error
|
var err error
|
||||||
encodedAuth, err = registryauth.EncodedAuth(dockerCLI, spec.TaskTemplate.ContainerSpec.Image)
|
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), spec.TaskTemplate.ContainerSpec.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/docker/cli/cli/command/service"
|
"github.com/docker/cli/cli/command/service"
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/cli/internal/registryauth"
|
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/moby/moby/api/types/container"
|
||||||
"github.com/moby/moby/api/types/network"
|
"github.com/moby/moby/api/types/network"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/moby/moby/api/types/swarm"
|
||||||
@@ -225,7 +224,7 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
|
|||||||
|
|
||||||
if sendAuth {
|
if sendAuth {
|
||||||
// Retrieve encoded auth token from the image reference
|
// Retrieve encoded auth token from the image reference
|
||||||
encodedAuth, err = registryauth.EncodedAuth(dockerCLI, image)
|
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ 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())
|
||||||
@@ -109,7 +113,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 {
|
||||||
@@ -120,6 +124,7 @@ 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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -255,7 +255,6 @@ 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())
|
||||||
@@ -263,7 +262,6 @@ 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)
|
||||||
@@ -271,7 +269,6 @@ 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"))
|
||||||
@@ -285,7 +282,6 @@ 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())
|
||||||
|
|||||||
@@ -27,8 +27,13 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
|
|||||||
}))
|
}))
|
||||||
cli.SetOut(outStream)
|
cli.SetOut(outStream)
|
||||||
|
|
||||||
r, _, err := os.Pipe()
|
r, w, 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)
|
||||||
@@ -58,8 +63,6 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -4156,6 +4156,7 @@ _docker_manifest_annotate() {
|
|||||||
amd64
|
amd64
|
||||||
arm
|
arm
|
||||||
arm64
|
arm64
|
||||||
|
loong64
|
||||||
mips64
|
mips64
|
||||||
mips64le
|
mips64le
|
||||||
ppc64le
|
ppc64le
|
||||||
|
|||||||
@@ -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.23
|
ARG ALPINE_VERSION=3.24
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
ARG GO_VERSION=1.26.10
|
ARG GO_VERSION=1.27.4
|
||||||
|
|
||||||
# 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.23
|
ARG ALPINE_VERSION=3.24
|
||||||
|
|
||||||
# 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.0
|
ARG BUILDX_VERSION=0.38.2
|
||||||
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
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
ARG GO_VERSION=1.26.10
|
ARG GO_VERSION=1.27.4
|
||||||
|
|
||||||
# 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.23
|
ARG ALPINE_VERSION=3.24
|
||||||
# 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.10.1
|
ARG GOLANGCI_LINT_VERSION=v2.14.0
|
||||||
|
|
||||||
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
|
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
ARG GO_VERSION=1.26.10
|
ARG GO_VERSION=1.27.4
|
||||||
|
|
||||||
# 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.23
|
ARG ALPINE_VERSION=3.24
|
||||||
ARG MODOUTDATED_VERSION=v0.8.0
|
ARG MODOUTDATED_VERSION=v0.9.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
|
||||||
|
|||||||
+1
-1364
File diff suppressed because it is too large
Load Diff
+2
-160
@@ -1,165 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
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 file is maintained within the docker/cli GitHub
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
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`.
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# 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.
|
||||||
+1
-262
@@ -7,265 +7,4 @@ aliases:
|
|||||||
- "/engine/extend/plugins_graphdriver/"
|
- "/engine/extend/plugins_graphdriver/"
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Installing and using a plugin](index.md#installing-and-using-a-plugin)
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
- [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`
|
|
||||||
|
|||||||
+1
-221
@@ -4,224 +4,4 @@ keywords: "API, Usage, plugins, documentation, developer"
|
|||||||
title: Plugin Config Version 1 of Plugin V2
|
title: Plugin Config Version 1 of Plugin V2
|
||||||
---
|
---
|
||||||
|
|
||||||
This document outlines the format of the V0 plugin configuration.
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
|
|
||||||
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.
|
Before Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -6,94 +6,4 @@ 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 document describes the Docker Engine plugins generally available in Docker
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
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).
|
|
||||||
|
|||||||
+1
-180
@@ -4,183 +4,4 @@ description: "How to write Docker plugins extensions "
|
|||||||
keywords: "API, Usage, plugins, documentation, developer"
|
keywords: "API, Usage, plugins, documentation, developer"
|
||||||
---
|
---
|
||||||
|
|
||||||
Docker plugins are out-of-process extensions which add capabilities to the
|
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.
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|||||||
@@ -6,280 +6,4 @@ aliases:
|
|||||||
- "/engine/extend/authorization/"
|
- "/engine/extend/authorization/"
|
||||||
---
|
---
|
||||||
|
|
||||||
This document describes the Docker Engine plugins available in Docker
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
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:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
@@ -4,212 +4,4 @@ description: "Log driver plugins."
|
|||||||
keywords: "Examples, Usage, plugins, docker, documentation, user guide, logging"
|
keywords: "Examples, Usage, plugins, docker, documentation, user guide, logging"
|
||||||
---
|
---
|
||||||
|
|
||||||
This document describes logging driver plugins for Docker.
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
@@ -4,76 +4,4 @@ description: "Metrics plugins."
|
|||||||
keywords: "Examples, Usage, plugins, docker, documentation, user guide, metrics"
|
keywords: "Examples, Usage, plugins, docker, documentation, user guide, metrics"
|
||||||
---
|
---
|
||||||
|
|
||||||
Docker exposes internal metrics based on the Prometheus format. Metrics plugins
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
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.
|
|
||||||
|
|||||||
@@ -4,61 +4,4 @@ description: "Network driver plugins."
|
|||||||
keywords: "Examples, Usage, plugins, docker, documentation, user guide"
|
keywords: "Examples, Usage, plugins, docker, documentation, user guide"
|
||||||
---
|
---
|
||||||
|
|
||||||
This document describes Docker Engine network driver plugins generally
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
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).
|
|
||||||
|
|||||||
@@ -3,182 +3,4 @@ keywords: "API, Usage, plugins, documentation, developer"
|
|||||||
title: Plugins and Services
|
title: Plugins and Services
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- This file is maintained within the docker/cli GitHub
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -4,362 +4,4 @@ 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"
|
||||||
---
|
---
|
||||||
|
|
||||||
Docker Engine volume plugins enable Engine deployments to be integrated with
|
This content has moved to the [github.com/moby/moby](https://github.com/moby/moby/blob/v2.0.0-beta.29/docs/extend/) repository.
|
||||||
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:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
||||||
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
197387f1b436 ubuntu:26.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:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
||||||
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
197387f1b436 ubuntu:26.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:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
||||||
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
197387f1b436 ubuntu:26.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:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
||||||
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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:24.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp
|
ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:26.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 `24.04` image:
|
Match containers based on the `ubuntu` version `26.04` image:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker ps --filter ancestor=ubuntu:24.04
|
$ docker ps --filter ancestor=ubuntu:26.04
|
||||||
|
|
||||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||||
82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
|
82a598284012 ubuntu:26.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:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
|
82a598284012 ubuntu:26.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Create time
|
#### Create time
|
||||||
|
|||||||
@@ -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:bookworm` image shares
|
Layers can be reused by images. For example, the `debian:trixie` image shares
|
||||||
its layer with the `debian:latest`. Pulling the `debian:bookworm` image therefore
|
its layer with the `debian:latest`. Pulling the `debian:trixie` 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:bookworm
|
$ docker image pull debian:trixie
|
||||||
|
|
||||||
bookworm: Pulling from library/debian
|
trixie: Pulling from library/debian
|
||||||
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
|
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
|
||||||
Status: Downloaded newer image for debian:bookworm
|
Status: Downloaded newer image for debian:trixie
|
||||||
docker.io/library/debian:bookworm
|
docker.io/library/debian:trixie
|
||||||
```
|
```
|
||||||
|
|
||||||
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 bookworm 4eacea30377a 8 days ago 124MB
|
debian trixie 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:bookworm` and `debian:latest` have the same image ID because they are
|
`debian:trixie` 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:24.04` pulls the latest version of the Ubuntu
|
For example, `docker pull ubuntu:26.04` pulls the latest version of the Ubuntu
|
||||||
24.04 image.
|
26.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:24.04` image from Docker Hub:
|
`ubuntu:26.04` image from Docker Hub:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker pull ubuntu:24.04
|
$ docker pull ubuntu:26.04
|
||||||
|
|
||||||
24.04: Pulling from library/ubuntu
|
26.04: Pulling from library/ubuntu
|
||||||
125a6e411906: Pull complete
|
125a6e411906: Pull complete
|
||||||
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
|
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
|
||||||
Status: Downloaded newer image for ubuntu:24.04
|
Status: Downloaded newer image for ubuntu:26.04
|
||||||
docker.io/library/ubuntu:24.04
|
docker.io/library/ubuntu:26.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 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
ubuntu 24.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||||
ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
ubuntu noble 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||||
ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
|
ubuntu 26.04 35a88802559d 6 weeks ago 78.1MB
|
||||||
ubuntu latest 35a88802559d 6 weeks ago 78.1MB
|
ubuntu latest 35a88802559d 6 weeks ago 78.1MB
|
||||||
ubuntu noble 35a88802559d 6 weeks ago 78.1MB
|
ubuntu resolute 35a88802559d 6 weeks ago 78.1MB
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cancel a pull
|
### Cancel a pull
|
||||||
|
|||||||
@@ -70,18 +70,6 @@ With Docker Desktop, the credential store is already installed and configured
|
|||||||
for you. Unless you want to change the credential store used by Docker Desktop,
|
for you. Unless you want to change the credential store used by Docker Desktop,
|
||||||
you can skip the following steps.
|
you can skip the following steps.
|
||||||
|
|
||||||
#### Docker Desktop sign-in
|
|
||||||
|
|
||||||
When Docker Desktop is running and you are signed in, commands that pull or
|
|
||||||
push images through the Docker Engine, such as `docker pull`, `docker push`,
|
|
||||||
`docker run`, and `docker service create`, use the Docker Desktop session for
|
|
||||||
Docker Hub (`docker.io`) and Docker Hardened Images (`dhi.io`). The session is
|
|
||||||
used before the credential store, credential helpers, and the configuration
|
|
||||||
file. Credentials set in the `DOCKER_AUTH_CONFIG` environment variable take
|
|
||||||
precedence over the Docker Desktop session. If Docker Desktop is not installed
|
|
||||||
or not running, or you are not signed in, the Docker CLI uses its own
|
|
||||||
credentials.
|
|
||||||
|
|
||||||
#### Configure the credential store
|
#### Configure the credential store
|
||||||
|
|
||||||
You need to specify the credential store in `$HOME/.docker/config.json`
|
You need to specify the credential store in `$HOME/.docker/config.json`
|
||||||
|
|||||||
@@ -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-24.04` |
|
| `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-26.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
|
||||||
|
|||||||
@@ -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:24.04)
|
2014-09-03T15:49:29.999999999Z07:00 container die 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 stop 4386fb97867d (image=ubuntu:26.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)
|
||||||
|
|
||||||
|
|||||||
+2
-1467
File diff suppressed because it is too large
Load Diff
+19
-19
@@ -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 `24.04` of the `ubuntu` image: `docker run ubuntu:24.04`.
|
run version `26.04` of the `ubuntu` image: `docker run ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it -m 300M --memory-swap -1 ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it -m 300M ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it -m 300M --memory-swap 1G ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it -m 500M --memory-reservation 200M ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --memory-reservation 1G ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it -m 100M --oom-kill-disable ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --oom-kill-disable ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --memory-swappiness=0 ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --cpuset-cpus="1,3" ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --cpuset-cpus="0-2" ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --cpuset-mems="1,3" ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --cpuset-mems="0-2" ubuntu:26.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:24.04 /bin/bash
|
$ docker run -it --name c1 --blkio-weight 300 ubuntu:26.04 /bin/bash
|
||||||
$ docker run -it --name c2 --blkio-weight 600 ubuntu:24.04 /bin/bash
|
$ docker run -it --name c2 --blkio-weight 600 ubuntu:26.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:24.04 ip link add dummy0 type dummy
|
$ docker run -it --rm ubuntu:26.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:24.04 ip link add dummy0 type dummy
|
$ docker run -it --rm --cap-add=NET_ADMIN ubuntu:26.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
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ const (
|
|||||||
IndexServer = "https://index.docker.io/v1/"
|
IndexServer = "https://index.docker.io/v1/"
|
||||||
// IndexName is the name of the index
|
// IndexName is the name of the index
|
||||||
IndexName = "docker.io"
|
IndexName = "docker.io"
|
||||||
DefaultRegistryHost = "registry-1.docker.io"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -62,7 +61,7 @@ var (
|
|||||||
// or "ubuntu:latest"), as well as references using the "docker.io" domain
|
// or "ubuntu:latest"), as well as references using the "docker.io" domain
|
||||||
// name, which is used as canonical reference for images on Docker Hub, but
|
// name, which is used as canonical reference for images on Docker Hub, but
|
||||||
// does not match the domain-name of Docker Hub's registry.
|
// does not match the domain-name of Docker Hub's registry.
|
||||||
DefaultV2Registry = &url.URL{Scheme: "https", Host: DefaultRegistryHost}
|
DefaultV2Registry = &url.URL{Scheme: "https", Host: "registry-1.docker.io"}
|
||||||
|
|
||||||
validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
|
validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
|
||||||
return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
|
return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
package registryauth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
configtypes "github.com/docker/cli/cli/config/types"
|
|
||||||
"github.com/docker/cli/internal/secretsengine"
|
|
||||||
"github.com/moby/moby/api/pkg/authconfig"
|
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
type configProvider interface {
|
|
||||||
ConfigFile() *configfile.ConfigFile
|
|
||||||
}
|
|
||||||
|
|
||||||
type authProvider interface {
|
|
||||||
RegistryAuthProvider() secretsengine.AuthProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
type envAuthFunc func(registryHostname string) (configtypes.AuthConfig, bool)
|
|
||||||
|
|
||||||
func EncodedAuth(dockerCLI configProvider, image string) (string, error) {
|
|
||||||
var auth secretsengine.AuthProvider = dockerCLI.ConfigFile()
|
|
||||||
if p, ok := dockerCLI.(authProvider); ok {
|
|
||||||
auth = p.RegistryAuthProvider()
|
|
||||||
}
|
|
||||||
return encodedAuth(envAuthConfig, auth, image)
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodedAuth(envAuth envAuthFunc, auth secretsengine.AuthProvider, image string) (string, error) {
|
|
||||||
registryRef, err := reference.ParseNormalizedNamed(image)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
domain := reference.Domain(registryRef)
|
|
||||||
authConfig, ok := envAuth(domain)
|
|
||||||
if !ok {
|
|
||||||
authConfig, err = auth.GetAuthConfig(domain)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return authconfig.Encode(registrytypes.AuthConfig{
|
|
||||||
Username: authConfig.Username,
|
|
||||||
Password: authConfig.Password,
|
|
||||||
ServerAddress: authConfig.ServerAddress,
|
|
||||||
Auth: authConfig.Auth,
|
|
||||||
IdentityToken: authConfig.IdentityToken,
|
|
||||||
RegistryToken: authConfig.RegistryToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func envAuthConfig(registryHostname string) (configtypes.AuthConfig, bool) {
|
|
||||||
if os.Getenv(configfile.DockerEnvConfigKey) == "" {
|
|
||||||
return configtypes.AuthConfig{}, false
|
|
||||||
}
|
|
||||||
authConfig, err := configfile.New("").GetAuthConfig(registryHostname)
|
|
||||||
return authConfig, err == nil && authConfig.Username != ""
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package registryauth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
configtypes "github.com/docker/cli/cli/config/types"
|
|
||||||
"github.com/docker/cli/internal/secretsengine"
|
|
||||||
"github.com/moby/moby/api/pkg/authconfig"
|
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
is "gotest.tools/v3/assert/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type recordingAuthProvider struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
hosts []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *recordingAuthProvider) GetAuthConfig(registryHostname string) (configtypes.AuthConfig, error) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
p.hosts = append(p.hosts, registryHostname)
|
|
||||||
return configtypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: registryHostname}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type configOnlyCli struct {
|
|
||||||
cfg *configfile.ConfigFile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *configOnlyCli) ConfigFile() *configfile.ConfigFile {
|
|
||||||
return c.cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeCli struct {
|
|
||||||
configOnlyCli
|
|
||||||
auth *recordingAuthProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeCli) RegistryAuthProvider() secretsengine.AuthProvider {
|
|
||||||
return c.auth
|
|
||||||
}
|
|
||||||
|
|
||||||
func noEnvAuth(string) (configtypes.AuthConfig, bool) {
|
|
||||||
return configtypes.AuthConfig{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig(t *testing.T) *configfile.ConfigFile {
|
|
||||||
t.Helper()
|
|
||||||
const configFileContent = `{"auths": {
|
|
||||||
"https://index.docker.io/v1/": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
|
||||||
"localhost:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
cfg := &configfile.ConfigFile{}
|
|
||||||
assert.NilError(t, cfg.LoadFromReader(bytes.NewReader([]byte(configFileContent))))
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodedAuthUsesCLIAuthProvider(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
dockerCLI := &fakeCli{configOnlyCli: configOnlyCli{cfg: configfile.New("")}, auth: &recordingAuthProvider{}}
|
|
||||||
|
|
||||||
actual, err := EncodedAuth(dockerCLI, "dhi.io/python")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
expected, err := authconfig.Encode(registrytypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: "dhi.io"})
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, is.Equal(actual, expected))
|
|
||||||
assert.Check(t, is.DeepEqual(dockerCLI.auth.hosts, []string{"dhi.io"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodedAuthWithoutAuthProviderUsesConfigFile(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
actual, err := EncodedAuth(&configOnlyCli{cfg: loadConfig(t)}, "ubuntu")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
expected, err := authconfig.Encode(registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"})
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, is.Equal(actual, expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodedAuthEnvTakesPrecedence(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
envAuth := func(registryHostname string) (configtypes.AuthConfig, bool) {
|
|
||||||
return configtypes.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: registryHostname}, true
|
|
||||||
}
|
|
||||||
auth := &recordingAuthProvider{}
|
|
||||||
|
|
||||||
actual, err := encodedAuth(envAuth, auth, "ubuntu")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
expected, err := authconfig.Encode(registrytypes.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: "docker.io"})
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, is.Equal(actual, expected))
|
|
||||||
assert.Check(t, is.Len(auth.hosts, 0), "the auth provider must not be asked for a registry in DOCKER_AUTH_CONFIG")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvAuthConfig(t *testing.T) {
|
|
||||||
t.Setenv(configfile.DockerEnvConfigKey, `{"auths":{"https://index.docker.io/v1/":{"auth":"ZW52dXNlcjplbnZwYXNz"}}}`)
|
|
||||||
|
|
||||||
got, ok := envAuthConfig("docker.io")
|
|
||||||
assert.Check(t, ok)
|
|
||||||
assert.Check(t, is.DeepEqual(got, configtypes.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: "https://index.docker.io/v1/"}))
|
|
||||||
|
|
||||||
_, ok = envAuthConfig("ghcr.io")
|
|
||||||
assert.Check(t, !ok)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodedAuthFromConfigFile(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
cfg := loadConfig(t)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
image string
|
|
||||||
expected registrytypes.AuthConfig
|
|
||||||
}{
|
|
||||||
{image: "ubuntu", expected: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"}},
|
|
||||||
{image: "localhost:5000/ubuntu", expected: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "localhost:5000"}},
|
|
||||||
{image: "no-auth.example.com/ubuntu", expected: registrytypes.AuthConfig{}},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.image, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
actual, err := encodedAuth(noEnvAuth, cfg, tc.image)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
expected, err := authconfig.Encode(tc.expected)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, is.Equal(actual, expected))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
Dial: (&net.Dialer{
|
DialContext: (&net.Dialer{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
}).Dial,
|
}).DialContext,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
TLSClientConfig: endpoint.TLSConfig,
|
TLSClientConfig: endpoint.TLSConfig,
|
||||||
DisableKeepAlives: true,
|
DisableKeepAlives: true,
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package secretsengine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config/credentials"
|
|
||||||
"github.com/docker/cli/cli/config/types"
|
|
||||||
"github.com/docker/cli/internal/registry"
|
|
||||||
seclient "github.com/docker/secrets-engine/client"
|
|
||||||
"github.com/docker/secrets-engine/client/dockerhub"
|
|
||||||
"github.com/docker/secrets-engine/x/api"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
lookupTimeout = 5 * time.Second
|
|
||||||
sessionExpirySkew = 30 * time.Second
|
|
||||||
sessionTTL = time.Minute
|
|
||||||
retryInterval = time.Minute
|
|
||||||
|
|
||||||
stagingRegistryHost = "registry-1-stage.docker.io"
|
|
||||||
dhiRegistryHost = "dhi.io"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errSessionExpired = errors.New("the session has expired (not signed in to Docker Desktop?)")
|
|
||||||
|
|
||||||
type hubClient interface {
|
|
||||||
GetDefaultSession(ctx context.Context) (dockerhub.UserSession, error)
|
|
||||||
GetDefaultProfile(ctx context.Context) (dockerhub.Profile, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ hubClient = dockerhub.ClientAuth(nil)
|
|
||||||
|
|
||||||
type AuthProvider interface {
|
|
||||||
GetAuthConfig(registryHostname string) (types.AuthConfig, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type provider struct {
|
|
||||||
fallback AuthProvider
|
|
||||||
hub *session
|
|
||||||
staging *session
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDesktop(fallback AuthProvider) AuthProvider {
|
|
||||||
return &provider{
|
|
||||||
fallback: fallback,
|
|
||||||
hub: newDesktopSession("Docker Hub"),
|
|
||||||
staging: newDesktopSession("Docker Hub staging", dockerhub.Staging()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provider) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
|
|
||||||
if s, serverAddress := p.sessionFor(registryHostname); s != nil {
|
|
||||||
if auth, ok := s.auth(); ok {
|
|
||||||
auth.ServerAddress = serverAddress
|
|
||||||
return auth, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p.fallback.GetAuthConfig(registryHostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provider) sessionFor(registryHostname string) (*session, string) {
|
|
||||||
switch credentials.ConvertToHostname(registryHostname) {
|
|
||||||
case registry.IndexName, registry.IndexHostname, registry.DefaultRegistryHost:
|
|
||||||
return p.hub, registry.IndexServer
|
|
||||||
case dhiRegistryHost:
|
|
||||||
return p.hub, dhiRegistryHost
|
|
||||||
case stagingRegistryHost:
|
|
||||||
return p.staging, stagingRegistryHost
|
|
||||||
default:
|
|
||||||
return nil, ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type session struct {
|
|
||||||
name string
|
|
||||||
connect func() (hubClient, error)
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
hub hubClient
|
|
||||||
retryAt time.Time
|
|
||||||
cached *types.AuthConfig
|
|
||||||
validUntil time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDesktopSession(name string, opts ...dockerhub.Option) *session {
|
|
||||||
return &session{
|
|
||||||
name: name,
|
|
||||||
connect: func() (hubClient, error) {
|
|
||||||
return connect(api.DesktopSocketPath(), opts...)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func connect(socketPath string, opts ...dockerhub.Option) (hubClient, error) {
|
|
||||||
if _, err := os.Lstat(socketPath); errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c, err := seclient.New(
|
|
||||||
seclient.WithSocketPath(socketPath),
|
|
||||||
seclient.WithTimeout(lookupTimeout),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c.HubAuth(opts...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *session) auth() (types.AuthConfig, bool) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if now.Before(s.retryAt) {
|
|
||||||
return types.AuthConfig{}, false
|
|
||||||
}
|
|
||||||
if s.cached != nil && now.Before(s.validUntil) {
|
|
||||||
return *s.cached, true
|
|
||||||
}
|
|
||||||
auth, validUntil, err := s.fetch()
|
|
||||||
if err != nil {
|
|
||||||
s.retryAt = time.Now().Add(retryInterval)
|
|
||||||
s.cached = nil
|
|
||||||
logFallback(s.name, err)
|
|
||||||
return types.AuthConfig{}, false
|
|
||||||
}
|
|
||||||
logrus.Debugf("using the %s session of %q from Docker Desktop", s.name, auth.Username)
|
|
||||||
s.cached, s.validUntil = &auth, validUntil
|
|
||||||
return auth, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *session) fetch() (types.AuthConfig, time.Time, error) {
|
|
||||||
if s.hub == nil {
|
|
||||||
hub, err := s.connect()
|
|
||||||
if err != nil {
|
|
||||||
return types.AuthConfig{}, time.Time{}, err
|
|
||||||
}
|
|
||||||
s.hub = hub
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), lookupTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
userSession, err := s.hub.GetDefaultSession(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return types.AuthConfig{}, time.Time{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
username := userSession.Claims.Username
|
|
||||||
if username == "" {
|
|
||||||
profile, err := s.hub.GetDefaultProfile(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return types.AuthConfig{}, time.Time{}, fmt.Errorf("resolving the Docker Hub username: %w", err)
|
|
||||||
}
|
|
||||||
username = profile.Username
|
|
||||||
}
|
|
||||||
if username == "" {
|
|
||||||
return types.AuthConfig{}, time.Time{}, errors.New("no username for the session")
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
validUntil := now.Add(sessionTTL)
|
|
||||||
if exp := userSession.Claims.ExpiresAt; exp != nil {
|
|
||||||
validUntil = exp.Add(-sessionExpirySkew)
|
|
||||||
if !now.Before(validUntil) {
|
|
||||||
return types.AuthConfig{}, time.Time{}, errSessionExpired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return types.AuthConfig{
|
|
||||||
Username: username,
|
|
||||||
Password: userSession.AccessToken,
|
|
||||||
}, validUntil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logFallback(name string, err error) {
|
|
||||||
if isSessionUnavailable(err) {
|
|
||||||
logrus.Debugf("no %s session available from Docker Desktop, using Docker CLI credentials: %v", name, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logrus.Warnf("could not use the %s session from Docker Desktop, using Docker CLI credentials instead: %v", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSessionUnavailable(err error) bool {
|
|
||||||
if _, ok := errors.AsType[*seclient.ConnectError](err); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return errors.Is(err, fs.ErrNotExist) ||
|
|
||||||
errors.Is(err, dockerhub.ErrNoSession) ||
|
|
||||||
errors.Is(err, errSessionExpired)
|
|
||||||
}
|
|
||||||
@@ -1,429 +0,0 @@
|
|||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package secretsengine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"testing/synctest"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config/types"
|
|
||||||
"github.com/docker/cli/internal/registry"
|
|
||||||
seclient "github.com/docker/secrets-engine/client"
|
|
||||||
"github.com/docker/secrets-engine/client/dockerhub"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeHub struct {
|
|
||||||
session dockerhub.UserSession
|
|
||||||
sessionErr error
|
|
||||||
profile dockerhub.Profile
|
|
||||||
profileErr error
|
|
||||||
sessionCalls atomic.Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeHub) GetDefaultSession(context.Context) (dockerhub.UserSession, error) {
|
|
||||||
f.sessionCalls.Add(1)
|
|
||||||
return f.session, f.sessionErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeHub) GetDefaultProfile(context.Context) (dockerhub.Profile, error) {
|
|
||||||
return f.profile, f.profileErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func hubSession(username string, expiresAt time.Time) dockerhub.UserSession {
|
|
||||||
s := dockerhub.UserSession{AccessToken: "desktop-oauth-token"}
|
|
||||||
s.Claims.Username = username
|
|
||||||
if !expiresAt.IsZero() {
|
|
||||||
s.Claims.ExpiresAt = &dockerhub.NumericDate{Time: expiresAt}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func inAnHour() time.Time {
|
|
||||||
return time.Now().Add(time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
type recordingFallback struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
hosts []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *recordingFallback) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
f.hosts = append(f.hosts, registryHostname)
|
|
||||||
return cliCredentials(registryHostname), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cliCredentials(registryHostname string) types.AuthConfig {
|
|
||||||
return types.AuthConfig{Username: "cli-user", Password: "cli-password", ServerAddress: registryHostname}
|
|
||||||
}
|
|
||||||
|
|
||||||
func desktopCredentials() types.AuthConfig {
|
|
||||||
return types.AuthConfig{Username: "hubuser", Password: "desktop-oauth-token", ServerAddress: registry.IndexServer}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stagingCredentials() types.AuthConfig {
|
|
||||||
return types.AuthConfig{Username: "stageuser", Password: "staging-oauth-token", ServerAddress: stagingRegistryHost}
|
|
||||||
}
|
|
||||||
|
|
||||||
type testProvider struct {
|
|
||||||
*provider
|
|
||||||
fallback *recordingFallback
|
|
||||||
staging *fakeHub
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestProvider(hub hubClient) *testProvider {
|
|
||||||
tp := &testProvider{
|
|
||||||
fallback: &recordingFallback{},
|
|
||||||
staging: &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile},
|
|
||||||
}
|
|
||||||
tp.provider = &provider{
|
|
||||||
fallback: tp.fallback,
|
|
||||||
hub: &session{name: "Docker Hub", hub: hub},
|
|
||||||
staging: &session{name: "Docker Hub staging", hub: tp.staging},
|
|
||||||
}
|
|
||||||
return tp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *testProvider) signInToStaging() {
|
|
||||||
tp.staging.sessionErr = nil
|
|
||||||
tp.staging.session = hubSession("stageuser", inAnHour())
|
|
||||||
tp.staging.session.AccessToken = "staging-oauth-token"
|
|
||||||
}
|
|
||||||
|
|
||||||
func connectError(reason seclient.ConnectReason) error {
|
|
||||||
return &seclient.ConnectError{
|
|
||||||
Reason: reason,
|
|
||||||
SocketPath: "/run/engine.sock",
|
|
||||||
Err: errors.New("dial unix /run/engine.sock"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewDesktopUsesSeparateSessions(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
p := NewDesktop(&recordingFallback{}).(*provider)
|
|
||||||
assert.Assert(t, p.hub != p.staging, "production and staging sessions must be separate")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDockerHubUsesDesktopSession(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
for _, host := range []string{"docker.io", "index.docker.io", "registry-1.docker.io", registry.IndexServer} {
|
|
||||||
t.Run(host, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig(host)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
assert.Equal(t, got.IdentityToken, "", "an IdentityToken is sent as a refresh token")
|
|
||||||
assert.Equal(t, len(p.fallback.hosts), 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOtherRegistriesUseFallback(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
hosts := []string{
|
|
||||||
"ghcr.io", "localhost:5000", "registry.example.com", "",
|
|
||||||
stagingRegistryHost + ".example.com", "example.com/" + stagingRegistryHost,
|
|
||||||
dhiRegistryHost + ".example.com", "example.com/" + dhiRegistryHost, "registry." + dhiRegistryHost,
|
|
||||||
"docker.io.example.com",
|
|
||||||
}
|
|
||||||
for _, host := range hosts {
|
|
||||||
t.Run(host, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
p.signInToStaging()
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig(host)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, cliCredentials(host))
|
|
||||||
assert.DeepEqual(t, p.fallback.hosts, []string{host})
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
|
|
||||||
assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDHIUsesDockerHubSession(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
for _, host := range []string{dhiRegistryHost, "https://" + dhiRegistryHost, "https://" + dhiRegistryHost + "/"} {
|
|
||||||
t.Run(host, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
p.signInToStaging()
|
|
||||||
|
|
||||||
want := desktopCredentials()
|
|
||||||
want.ServerAddress = dhiRegistryHost
|
|
||||||
got, err := p.GetAuthConfig(host)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, want)
|
|
||||||
assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "the staging session must not be read for DHI")
|
|
||||||
|
|
||||||
got, err = p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStagingRegistryUsesStagingSession(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
for _, host := range []string{stagingRegistryHost, "https://" + stagingRegistryHost + "/"} {
|
|
||||||
t.Run(host, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
p.signInToStaging()
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig(host)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, stagingCredentials())
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(0), "the production session must not be read for the staging registry")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvironmentsDoNotMix(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
t.Run("staging session is not used for docker.io", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
p := newTestProvider(&fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile})
|
|
||||||
p.signInToStaging()
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
|
||||||
|
|
||||||
got, err = p.GetAuthConfig(stagingRegistryHost)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, stagingCredentials())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("production session is not used for the staging registry", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
p := newTestProvider(&fakeHub{session: hubSession("hubuser", inAnHour())})
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig(stagingRegistryHost)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, cliCredentials(stagingRegistryHost))
|
|
||||||
|
|
||||||
got, err = p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsernameFromProfile(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
hub := &fakeHub{
|
|
||||||
session: hubSession("", inAnHour()),
|
|
||||||
profile: dockerhub.Profile{Username: "hubuser"},
|
|
||||||
}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFallsBackWithoutUsableSession(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
hub func() *fakeHub
|
|
||||||
}{
|
|
||||||
{name: "secrets engine not running", hub: func() *fakeHub { return &fakeHub{sessionErr: connectError(seclient.ReasonNotRunning)} }},
|
|
||||||
{name: "secrets engine socket not accessible", hub: func() *fakeHub { return &fakeHub{sessionErr: connectError(seclient.ReasonPermissionDenied)} }},
|
|
||||||
{name: "nobody signed in", hub: func() *fakeHub { return &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile} }},
|
|
||||||
{name: "access denied", hub: func() *fakeHub { return &fakeHub{sessionErr: seclient.ErrAccessDenied} }},
|
|
||||||
{name: "no username", hub: func() *fakeHub { return &fakeHub{session: hubSession("", inAnHour())} }},
|
|
||||||
{name: "profile lookup fails", hub: func() *fakeHub {
|
|
||||||
return &fakeHub{session: hubSession("", inAnHour()), profileErr: errors.New("boom")}
|
|
||||||
}},
|
|
||||||
{name: "expired", hub: func() *fakeHub { return &fakeHub{session: hubSession("hubuser", time.Now().Add(-time.Minute))} }},
|
|
||||||
{name: "expires within the skew", hub: func() *fakeHub {
|
|
||||||
return &fakeHub{session: hubSession("hubuser", time.Now().Add(sessionExpirySkew/2))}
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
|
||||||
hub := tc.hub()
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
|
|
||||||
for range 2 {
|
|
||||||
got, err := p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
|
||||||
}
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1), "Desktop must not be asked again within retryInterval")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDesktopFallsBackWhenClientCannotBeCreated(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
var connects atomic.Int32
|
|
||||||
s := &session{
|
|
||||||
name: "Docker Hub",
|
|
||||||
connect: func() (hubClient, error) {
|
|
||||||
connects.Add(1)
|
|
||||||
return nil, errors.New("no socket path")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
p := &provider{fallback: &recordingFallback{}, hub: s}
|
|
||||||
|
|
||||||
for range 2 {
|
|
||||||
got, err := p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
|
||||||
}
|
|
||||||
assert.Equal(t, connects.Load(), int32(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRetriesAfterInterval(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
|
||||||
hub := &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
|
||||||
|
|
||||||
hub.sessionErr = nil
|
|
||||||
hub.session = hubSession("hubuser", inAnHour())
|
|
||||||
|
|
||||||
time.Sleep(retryInterval - time.Second)
|
|
||||||
got, err = p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
got, err = p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReusesSessionUntilExpiry(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
|
||||||
expiresAt := time.Now().Add(10 * time.Minute)
|
|
||||||
hub := &fakeHub{session: hubSession("hubuser", expiresAt)}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
time.Sleep(5 * time.Minute)
|
|
||||||
got, err = p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
|
||||||
|
|
||||||
hub.session = hubSession("hubuser", expiresAt.Add(time.Hour))
|
|
||||||
hub.session.AccessToken = "refreshed-token"
|
|
||||||
time.Sleep(5*time.Minute - sessionExpirySkew)
|
|
||||||
|
|
||||||
got, err = p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Equal(t, got.Password, "refreshed-token")
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRefetchesSessionWithoutExpiry(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
|
||||||
hub := &fakeHub{session: hubSession("hubuser", time.Time{})}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
|
|
||||||
got, err := p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
time.Sleep(sessionTTL - time.Second)
|
|
||||||
got, err = p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
got, err = p.GetAuthConfig("docker.io")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, got, desktopCredentials())
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConcurrentLookupsShareOneFetch(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
|
|
||||||
p := newTestProvider(hub)
|
|
||||||
|
|
||||||
const lookups = 20
|
|
||||||
results := make([]types.AuthConfig, lookups)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for i := range lookups {
|
|
||||||
wg.Go(func() {
|
|
||||||
auth, err := p.GetAuthConfig("docker.io")
|
|
||||||
assert.Check(t, err)
|
|
||||||
results[i] = auth
|
|
||||||
})
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
for _, auth := range results {
|
|
||||||
assert.DeepEqual(t, auth, desktopCredentials())
|
|
||||||
}
|
|
||||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectSkipsMissingSocket(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
_, err := connect(filepath.Join(t.TempDir(), "engine.sock"))
|
|
||||||
assert.Assert(t, errors.Is(err, fs.ErrNotExist), err)
|
|
||||||
assert.Assert(t, isSessionUnavailable(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsSessionUnavailable(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
for _, err := range []error{
|
|
||||||
connectError(seclient.ReasonNotRunning),
|
|
||||||
fmt.Errorf("get session: %w", connectError(seclient.ReasonNotRunning)),
|
|
||||||
connectError(seclient.ReasonPermissionDenied),
|
|
||||||
connectError(seclient.ReasonTimeout),
|
|
||||||
connectError(seclient.ReasonUnknown),
|
|
||||||
dockerhub.ErrNoSession,
|
|
||||||
dockerhub.ErrNoDefaultProfile,
|
|
||||||
errSessionExpired,
|
|
||||||
fs.ErrNotExist,
|
|
||||||
} {
|
|
||||||
assert.Assert(t, isSessionUnavailable(err), err)
|
|
||||||
}
|
|
||||||
for _, err := range []error{
|
|
||||||
seclient.ErrAccessDenied,
|
|
||||||
errors.New("no username for the session"),
|
|
||||||
context.DeadlineExceeded,
|
|
||||||
} {
|
|
||||||
assert.Assert(t, !isSessionUnavailable(err), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/registryclient"
|
"github.com/docker/cli/internal/registryclient"
|
||||||
"github.com/docker/cli/internal/secretsengine"
|
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -177,10 +176,6 @@ func (c *FakeCli) SetRegistryClient(registryClient registryclient.RegistryClient
|
|||||||
c.registryClient = registryClient
|
c.registryClient = registryClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *FakeCli) RegistryAuthProvider() secretsengine.AuthProvider {
|
|
||||||
return c.configfile
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildKitEnabled on the fake cli
|
// BuildKitEnabled on the fake cli
|
||||||
func (*FakeCli) BuildKitEnabled() (bool, error) {
|
func (*FakeCli) BuildKitEnabled() (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|||||||
@@ -27,8 +27,13 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
|
|||||||
}))
|
}))
|
||||||
cli.SetOut(outStream)
|
cli.SetOut(outStream)
|
||||||
|
|
||||||
r, _, err := os.Pipe()
|
r, w, 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)
|
||||||
@@ -58,8 +63,6 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:24.04 /bin/sh -c #(nop) MA 20 minutes ago Exit 0 desperate_brattain
|
a87ecb4f327c ubuntu:26.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:24.04 /bin/sh -c #(nop) MA 2 weeks ago Exit 0 drunk_pike
|
41d50ecd2f57 ubuntu:26.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
@@ -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 24.04 or 22.04, etc. to manage different
|
called ubuntu. It may be tagged with 26.04 or 24.04, etc. to manage different
|
||||||
versions.
|
versions.
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
|
|||||||
+19
-19
@@ -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:bookworm` image shares
|
Layers can be reused by images. For example, the `debian:trixie` image shares
|
||||||
its layer with the `debian:latest`. Pulling the `debian:bookworm` image therefore
|
its layer with the `debian:latest`. Pulling the `debian:trixie` 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:bookworm
|
$ docker image pull debian:trixie
|
||||||
|
|
||||||
bookworm: Pulling from library/debian
|
trixie: Pulling from library/debian
|
||||||
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
|
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
|
||||||
Status: Downloaded newer image for debian:bookworm
|
Status: Downloaded newer image for debian:trixie
|
||||||
docker.io/library/debian:bookworm
|
docker.io/library/debian:trixie
|
||||||
|
|
||||||
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 bookworm 4eacea30377a 8 days ago 124MB
|
debian trixie 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:bookworm` and `debian:latest` have the same image ID because they are
|
`debian:trixie` 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:24.04` pulls the latest version of the Ubuntu
|
For example, `docker image pull ubuntu:26.04` pulls the latest version of the Ubuntu
|
||||||
24.04 image.
|
26.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:24.04` image from Docker Hub:
|
`ubuntu:26.04` image from Docker Hub:
|
||||||
|
|
||||||
$ docker image pull ubuntu:24.04
|
$ docker image pull ubuntu:26.04
|
||||||
|
|
||||||
24.04: Pulling from library/ubuntu
|
26.04: Pulling from library/ubuntu
|
||||||
125a6e411906: Pull complete
|
125a6e411906: Pull complete
|
||||||
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
|
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
|
||||||
Status: Downloaded newer image for ubuntu:24.04
|
Status: Downloaded newer image for ubuntu:26.04
|
||||||
docker.io/library/ubuntu:24.04
|
docker.io/library/ubuntu:26.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 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
ubuntu 24.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||||
ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
ubuntu noble 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||||
ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
|
ubuntu 26.04 35a88802559d 6 weeks ago 78.1MB
|
||||||
ubuntu latest 35a88802559d 6 weeks ago 78.1MB
|
ubuntu latest 35a88802559d 6 weeks ago 78.1MB
|
||||||
ubuntu noble 35a88802559d 6 weeks ago 78.1MB
|
ubuntu resolute 35a88802559d 6 weeks ago 78.1MB
|
||||||
|
|
||||||
## Cancel a pull
|
## Cancel a pull
|
||||||
|
|
||||||
|
|||||||
@@ -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:24.04) die
|
2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu:26.04) die
|
||||||
2015-05-12T15:52:12.999999999Z07:00 4386fb97867d: (from ubuntu:24.04) stop
|
2015-05-12T15:52:12.999999999Z07:00 4386fb97867d: (from ubuntu:26.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:24.04)
|
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.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:24.04'
|
$ docker events --filter '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 start 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 die 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 stop 4386fb97867d (image=ubuntu:26.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:24.04)
|
2014-09-03T15:49:29.999999999Z07:00 container die 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 stop 4386fb97867d (image=ubuntu:26.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)
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ Server:
|
|||||||
seccomp
|
seccomp
|
||||||
Profile: builtin
|
Profile: builtin
|
||||||
cgroupns
|
cgroupns
|
||||||
Kernel Version: 5.15.0-25-generic
|
Kernel Version: 6.8.0-31-generic
|
||||||
Operating System: Ubuntu 22.04 LTS
|
Operating System: Ubuntu 24.04 LTS
|
||||||
OSType: linux
|
OSType: linux
|
||||||
Architecture: x86_64
|
Architecture: x86_64
|
||||||
CPUs: 1
|
CPUs: 1
|
||||||
|
|||||||
+3
-11
@@ -33,8 +33,6 @@ 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()
|
||||||
@@ -48,20 +46,14 @@ 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
|
||||||
|
|||||||
@@ -52,3 +52,13 @@ func TestGpusOpts(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGpusOptDuplicateCount(t *testing.T) {
|
||||||
|
for _, value := range []string{"count=1,2", "1,2", "all,1", "count=1,all", "1,count=2", "count=1,count=2"} {
|
||||||
|
t.Run(value, func(t *testing.T) {
|
||||||
|
var gpus GpuOpts
|
||||||
|
assert.Error(t, gpus.Set(value), "gpu request key 'count' can be specified only once")
|
||||||
|
assert.Assert(t, is.Len(gpus.Value(), 0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,13 +88,20 @@ if [ "$CGO_ENABLED" = "1" ] && [ "$(go env GOOS)" != "windows" ]; then
|
|||||||
fi
|
fi
|
||||||
export GO_BUILDMODE
|
export GO_BUILDMODE
|
||||||
|
|
||||||
GO_LDFLAGS="${GO_LDFLAGS:-}"
|
# Omit DWARF debug information to reduce binary size, but retain the symbol
|
||||||
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.GitCommit=${GITCOMMIT}\""
|
# table for tools that inspect Go binaries. Pass GO_LDFLAGS="-w=0" to retain DWARF.
|
||||||
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.BuildTime=${BUILDTIME}\""
|
GO_LDFLAGS="-w ${GO_LDFLAGS:-}"
|
||||||
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.Version=${VERSION}\""
|
|
||||||
if test -n "${PLATFORM}"; then
|
# Quotes are interpreted by Go's -ldflags parser, not the shell.
|
||||||
|
# shellcheck disable=SC2089
|
||||||
|
{
|
||||||
|
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.GitCommit=${GITCOMMIT}\""
|
||||||
|
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.BuildTime=${BUILDTIME}\""
|
||||||
|
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.Version=${VERSION}\""
|
||||||
|
if test -n "${PLATFORM}"; then
|
||||||
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.PlatformName=${PLATFORM}\""
|
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.PlatformName=${PLATFORM}\""
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
if [ "$CGO_ENABLED" = "1" ] && [ "$GO_LINKMODE" = "static" ] && [ "$(go env GOOS)" = "linux" ]; then
|
if [ "$CGO_ENABLED" = "1" ] && [ "$GO_LINKMODE" = "static" ] && [ "$(go env GOOS)" = "linux" ]; then
|
||||||
GO_LDFLAGS="$GO_LDFLAGS -linkmode external -extldflags -static"
|
GO_LDFLAGS="$GO_LDFLAGS -linkmode external -extldflags -static"
|
||||||
fi
|
fi
|
||||||
@@ -128,7 +135,7 @@ if [ -n "$GO_STRIP" ]; then
|
|||||||
# /usr/local/go/pkg/tool/linux_amd64/link: /usr/local/go/pkg/tool/linux_amd64/link: running strip failed: exit status 1
|
# /usr/local/go/pkg/tool/linux_amd64/link: /usr/local/go/pkg/tool/linux_amd64/link: running strip failed: exit status 1
|
||||||
# llvm-strip: error: unsupported load command (cmd=0x5)
|
# llvm-strip: error: unsupported load command (cmd=0x5)
|
||||||
# more info: https://github.com/docker/cli/pull/3717
|
# more info: https://github.com/docker/cli/pull/3717
|
||||||
GO_LDFLAGS="$GO_LDFLAGS -s -w"
|
GO_LDFLAGS="$GO_LDFLAGS -s"
|
||||||
fi
|
fi
|
||||||
export GO_LDFLAGS="$GO_LDFLAGS" # https://github.com/koalaman/shellcheck/issues/2064
|
export GO_LDFLAGS="$GO_LDFLAGS" # https://github.com/koalaman/shellcheck/issues/2064
|
||||||
|
|
||||||
|
|||||||
+28
-30
@@ -4,7 +4,7 @@ module github.com/docker/cli
|
|||||||
// There is no 'go.mod' file, as that would imply opting in for all the rules
|
// There is no 'go.mod' file, as that would imply opting in for all the rules
|
||||||
// around SemVer, which this repo cannot abide by as it uses CalVer.
|
// around SemVer, which this repo cannot abide by as it uses CalVer.
|
||||||
|
|
||||||
go 1.26.10
|
go 1.26.0
|
||||||
|
|
||||||
tool (
|
tool (
|
||||||
github.com/cpuguy83/go-md2man/v2 // for scripts/docs/generate-man.sh
|
github.com/cpuguy83/go-md2man/v2 // for scripts/docs/generate-man.sh
|
||||||
@@ -21,13 +21,11 @@ require (
|
|||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
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/docker-credential-helpers v0.9.9
|
github.com/docker/docker-credential-helpers v0.9.13
|
||||||
github.com/docker/go-connections v0.8.2
|
github.com/docker/go-connections v0.8.2
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
github.com/docker/secrets-engine/client v0.2.2
|
|
||||||
github.com/docker/secrets-engine/x v0.8.5
|
|
||||||
github.com/fvbommel/sortorder v1.2.1
|
github.com/fvbommel/sortorder v1.2.1
|
||||||
github.com/go-jose/go-jose/v4 v4.1.5
|
github.com/go-jose/go-jose/v4 v4.1.7
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||||
github.com/gogo/protobuf v1.3.2
|
github.com/gogo/protobuf v1.3.2
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
@@ -35,12 +33,12 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mattn/go-runewidth v0.0.31
|
github.com/mattn/go-runewidth v0.0.31
|
||||||
github.com/moby/go-archive v0.3.3
|
github.com/moby/go-archive v0.3.3
|
||||||
github.com/moby/moby/api v1.56.1
|
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131
|
||||||
github.com/moby/moby/client v0.6.2
|
github.com/moby/moby/client v0.6.3-0.20261106011722-402d0a16f131
|
||||||
github.com/moby/patternmatcher v0.6.1
|
github.com/moby/patternmatcher v0.6.1
|
||||||
github.com/moby/swarmkit/v2 v2.1.2
|
github.com/moby/swarmkit/v2 v2.1.3
|
||||||
github.com/moby/sys/atomicwriter v0.1.0
|
github.com/moby/sys/atomicwriter v0.1.0
|
||||||
github.com/moby/sys/capability v0.4.0
|
github.com/moby/sys/capability v0.5.0
|
||||||
github.com/moby/sys/sequential v0.7.0
|
github.com/moby/sys/sequential v0.7.0
|
||||||
github.com/moby/sys/signal v0.7.1
|
github.com/moby/sys/signal v0.7.1
|
||||||
github.com/moby/sys/symlink v0.3.0
|
github.com/moby/sys/symlink v0.3.0
|
||||||
@@ -54,26 +52,25 @@ require (
|
|||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0
|
||||||
go.opentelemetry.io/otel v1.46.0
|
go.opentelemetry.io/otel v1.47.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0
|
||||||
go.opentelemetry.io/otel/metric v1.46.0
|
go.opentelemetry.io/otel/metric v1.47.0
|
||||||
go.opentelemetry.io/otel/sdk v1.46.0
|
go.opentelemetry.io/otel/sdk v1.47.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.46.0
|
go.opentelemetry.io/otel/sdk/metric v1.47.0
|
||||||
go.opentelemetry.io/otel/trace v1.46.0
|
go.opentelemetry.io/otel/trace v1.47.0
|
||||||
go.yaml.in/yaml/v3 v3.0.5
|
go.yaml.in/yaml/v3 v3.0.6
|
||||||
golang.org/x/sync v0.23.0
|
golang.org/x/sync v0.25.0
|
||||||
golang.org/x/sys v0.48.0
|
golang.org/x/sys v0.50.0
|
||||||
golang.org/x/term v0.46.0
|
golang.org/x/term v0.48.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.44.0
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
tags.cncf.io/container-device-interface v1.1.1
|
tags.cncf.io/container-device-interface v1.1.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
connectrpc.com/connect v1.19.1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20261013165250-51075c6ea6bc // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
|
|
||||||
github.com/Microsoft/go-winio v0.6.3 // indirect
|
github.com/Microsoft/go-winio v0.6.3 // 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
|
||||||
@@ -90,7 +87,7 @@ require (
|
|||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.20.1 // indirect
|
github.com/klauspost/compress v1.20.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/user v0.4.1 // indirect
|
github.com/moby/sys/user v0.4.1 // indirect
|
||||||
github.com/moby/sys/userns v0.2.1 // indirect
|
github.com/moby/sys/userns v0.2.1 // indirect
|
||||||
@@ -102,13 +99,14 @@ require (
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
go.etcd.io/raft/v3 v3.6.0 // indirect
|
go.etcd.io/raft/v3 v3.6.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/log v1.47.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.41.0 // indirect
|
golang.org/x/mod v0.43.0 // indirect
|
||||||
golang.org/x/net v0.60.0 // indirect
|
golang.org/x/net v0.62.0 // indirect
|
||||||
golang.org/x/time v0.16.0 // indirect
|
golang.org/x/time v0.16.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||||
google.golang.org/grpc v1.83.2 // indirect
|
google.golang.org/grpc v1.83.2 // indirect
|
||||||
google.golang.org/protobuf v1.36.12 // indirect
|
google.golang.org/protobuf v1.36.12 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+56
-62
@@ -1,11 +1,9 @@
|
|||||||
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
|
|
||||||
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
|
github.com/Azure/go-ansiterm v0.0.0-20261013165250-51075c6ea6bc h1:58s5YzGPNAP7+e1uemaL0ax/NPSeJOHdRpRIowd+JDk=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
|
github.com/Azure/go-ansiterm v0.0.0-20261013165250-51075c6ea6bc/go.mod h1:DMNF6Kxv+EcuUvVuNkvC3f+BjEEmc/Fn9aU+qiD8zpU=
|
||||||
github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
|
github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
|
||||||
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -37,8 +35,8 @@ github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQt
|
|||||||
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
|
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
|
||||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w=
|
github.com/docker/docker-credential-helpers v0.9.13 h1:yLE5jCyKJraJaBACIGqgpLX0UheWfZUwBkIeCu6WPFU=
|
||||||
github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
github.com/docker/docker-credential-helpers v0.9.13/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||||
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
|
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
|
||||||
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||||
github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0=
|
github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0=
|
||||||
@@ -49,16 +47,12 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
|||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
||||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||||
github.com/docker/secrets-engine/client v0.2.2 h1:aPvmlzsxNA+C+xgp2F+WiiDbCSAXHC591HscFFOvPOE=
|
|
||||||
github.com/docker/secrets-engine/client v0.2.2/go.mod h1:c3l/N+fA0kiy9S8BE3kKrO5hOZu4+SLYZK9Jx+jc2cw=
|
|
||||||
github.com/docker/secrets-engine/x v0.8.5 h1:P7D/YESMjpX18S4ratBgoeJkAFIxgsPxe093uFmgiGc=
|
|
||||||
github.com/docker/secrets-engine/x v0.8.5/go.mod h1:jQ5CXCYCPn8Sw96TS8Ufq3oyIGH2GzUtVXIdwa2Q0VU=
|
|
||||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||||
github.com/fvbommel/sortorder v1.2.1 h1:ASc85RbthwLEqzuiCk7WQQiopfyGWQiEYZVJ3Qa4A+U=
|
github.com/fvbommel/sortorder v1.2.1 h1:ASc85RbthwLEqzuiCk7WQQiopfyGWQiEYZVJ3Qa4A+U=
|
||||||
github.com/fvbommel/sortorder v1.2.1/go.mod h1:LbhO04ijZIeUuvz9B9BkI/qYrpZZEn1gWhxv4QjUKVs=
|
github.com/fvbommel/sortorder v1.2.1/go.mod h1:LbhO04ijZIeUuvz9B9BkI/qYrpZZEn1gWhxv4QjUKVs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
|
github.com/go-jose/go-jose/v4 v4.1.7 h1:Nac/98uT5LPkfbiFuYa8ktf62a2VxwQ4xbq1GK8pV5E=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.7/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
@@ -84,8 +78,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
github.com/klauspost/compress v1.20.2 h1:v5tiZvm1KGLyg5d5oXdXe3lsSPrbEUiKoXGia//I7Qo=
|
||||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
github.com/klauspost/compress v1.20.2/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo=
|
github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo=
|
||||||
@@ -94,18 +88,18 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
|||||||
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/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
|
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
|
||||||
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||||
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
|
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131 h1:tPz9ahj8F7AjQw62XQFP65kmoxF3rXTYSit3q06/yQc=
|
||||||
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131/go.mod h1:pRZ1QRtx6AVhdckeFYo4DUX4ASNF0SCllhyY6aj2mgg=
|
||||||
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4=
|
github.com/moby/moby/client v0.6.3-0.20261106011722-402d0a16f131 h1:ReIO7lalQVE4BetZuIvrxScg8Rxv4Sk74XRs06ryRDU=
|
||||||
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
|
github.com/moby/moby/client v0.6.3-0.20261106011722-402d0a16f131/go.mod h1:dPyRteJDuLdP8to3kWmNAU/tbMwSFCE88yKHvY/nZ9k=
|
||||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
|
github.com/moby/swarmkit/v2 v2.1.3 h1:UcwtwkIRjr75qSgUWCTIuzKUzAXlidxsiqVMnyxSF1w=
|
||||||
github.com/moby/swarmkit/v2 v2.1.2/go.mod h1:GQ6T0ij2oBbWX10OHwpvK449xfXkQMZ8J+B+eQ4mgp4=
|
github.com/moby/swarmkit/v2 v2.1.3/go.mod h1:KhkxT93Hm4wHoNUPJ4JRD384pS7OODFGzJWoDRj9bLE=
|
||||||
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/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
|
github.com/moby/sys/capability v0.5.0 h1:Bm4oD9xGCtZguPnaoQy1/jvKTSwZHMvM0FrnxNYtGYU=
|
||||||
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
|
github.com/moby/sys/capability v0.5.0/go.mod h1:i+0cXvNSriGrL3C6sxnmlmGL5HVU0OdxocizXdC8A+g=
|
||||||
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
|
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
|
||||||
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
|
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
|
||||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||||
@@ -161,63 +155,65 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
|
|||||||
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
|
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw=
|
||||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
|
||||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 h1:LaLVGJtIzHcvHjVs2OftmKcc+M9mdOqS8vZ1fBD1Yes=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0/go.mod h1:vF6H5sNDuQBJWgSX00iZgbIqk2MZZoVMH0ar5t5bOpw=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc=
|
||||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
|
||||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
|
||||||
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
|
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
|
||||||
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
|
go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
|
||||||
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
|
go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
|
||||||
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
|
go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
|
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
|
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
|
||||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
|
||||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
|
||||||
|
go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
|
||||||
|
go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
|
||||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
go.yaml.in/yaml/v3 v3.0.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM=
|
||||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
go.yaml.in/yaml/v3 v3.0.6/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
golang.org/x/mod v0.43.0 h1:MFuq5J6s35W9D3IjkwyWc9vHh0nENOrt5K8Iof4ofmg=
|
||||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
golang.org/x/mod v0.43.0/go.mod h1:4ec1JnuyBz5+FIVhg86UsA39YG8BpnrZdJYSNHmGzHY=
|
||||||
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.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
golang.org/x/sync v0.25.0 h1:dJnifr0nMV8/SBnBvdo4eSGGnrc3O4FFx6IqvtLcmRs=
|
||||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
golang.org/x/sync v0.25.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
golang.org/x/sys v0.50.0 h1:dkKZEFTEKaLr1sjhs0WiKqoi8Jj5nHbS3Njmx1UoMJs=
|
||||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
golang.org/x/sys v0.50.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
golang.org/x/term v0.48.0 h1:G6UU1eiSB8nl3DzeUGt7Mg+2SctI8tPS6Z5/mBt6cOk=
|
||||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
golang.org/x/term v0.48.0/go.mod h1:BRDLHCZr2SUjIeBHHpRyQeC9xzSAVbzY2rvKgafnHf4=
|
||||||
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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
golang.org/x/text v0.44.0 h1:YItdp/mQNGnsmrgpcdA5yQ3bnRxCfo/Tj/iByGW9GGo=
|
||||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
golang.org/x/text v0.44.0/go.mod h1:S4FR1waQ9LbjQTls97SyRjDaUUVkaM+cXwwuG/G+MwY=
|
||||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -230,10 +226,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 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=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
|
||||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
@@ -241,7 +237,5 @@ google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
|
||||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
|
||||||
tags.cncf.io/container-device-interface v1.1.1 h1:YPwQz4xg8PlQ0yT/baR0BtLpTQROe4l6M1yuRgAu1vc=
|
tags.cncf.io/container-device-interface v1.1.1 h1:YPwQz4xg8PlQ0yT/baR0BtLpTQROe4l6M1yuRgAu1vc=
|
||||||
tags.cncf.io/container-device-interface v1.1.1/go.mod h1:S1PSJWYPD4Iom0/39mvr/VVFCfG0Yt14j20d0OYrY2M=
|
tags.cncf.io/container-device-interface v1.1.1/go.mod h1:S1PSJWYPD4Iom0/39mvr/VVFCfG0Yt14j20d0OYrY2M=
|
||||||
|
|||||||
Generated
-5
@@ -1,5 +0,0 @@
|
|||||||
/.tmp/
|
|
||||||
*.pprof
|
|
||||||
*.svg
|
|
||||||
cover.out
|
|
||||||
connect.test
|
|
||||||
-130
@@ -1,130 +0,0 @@
|
|||||||
linters-settings:
|
|
||||||
errcheck:
|
|
||||||
check-type-assertions: true
|
|
||||||
exhaustruct:
|
|
||||||
include:
|
|
||||||
# No zero values for param structs.
|
|
||||||
- 'connectrpc\.com/connect\..*[pP]arams'
|
|
||||||
forbidigo:
|
|
||||||
forbid:
|
|
||||||
- '^fmt\.Print'
|
|
||||||
- '^log\.'
|
|
||||||
- '^print$'
|
|
||||||
- '^println$'
|
|
||||||
- '^panic$'
|
|
||||||
godox:
|
|
||||||
# TODO, OPT, etc. comments are fine to commit. Use FIXME comments for
|
|
||||||
# temporary hacks, and use godox to prevent committing them.
|
|
||||||
keywords: [FIXME]
|
|
||||||
importas:
|
|
||||||
no-unaliased: true
|
|
||||||
alias:
|
|
||||||
- pkg: connectrpc.com/connect/internal/gen/connect/ping/v1
|
|
||||||
alias: pingv1
|
|
||||||
varnamelen:
|
|
||||||
ignore-decls:
|
|
||||||
- T any
|
|
||||||
- i int
|
|
||||||
- wg sync.WaitGroup
|
|
||||||
linters:
|
|
||||||
enable-all: true
|
|
||||||
disable:
|
|
||||||
- cyclop # covered by gocyclo
|
|
||||||
- depguard # unnecessary for small libraries
|
|
||||||
- funlen # rely on code review to limit function length
|
|
||||||
- gocognit # dubious "cognitive overhead" quantification
|
|
||||||
- gofumpt # prefer standard gofmt
|
|
||||||
- goimports # rely on gci instead
|
|
||||||
- inamedparam # convention is not followed
|
|
||||||
- ireturn # "accept interfaces, return structs" isn't ironclad
|
|
||||||
- lll # don't want hard limits for line length
|
|
||||||
- maintidx # covered by gocyclo
|
|
||||||
- mnd # status codes are clearer than constants
|
|
||||||
- nlreturn # generous whitespace violates house style
|
|
||||||
- nonamedreturns # named returns are fine; it's *bare* returns that are bad
|
|
||||||
- protogetter # too many false positives
|
|
||||||
- tenv # replaced by usetesting
|
|
||||||
- testpackage # internal tests are fine
|
|
||||||
- wrapcheck # don't _always_ need to wrap errors
|
|
||||||
- wsl # generous whitespace violates house style
|
|
||||||
issues:
|
|
||||||
exclude-dirs-use-default: false
|
|
||||||
|
|
||||||
exclude:
|
|
||||||
# Don't ban use of fmt.Errorf to create new errors, but the remaining
|
|
||||||
# checks from err113 are useful.
|
|
||||||
- "do not define dynamic errors, use wrapped static errors instead: .*"
|
|
||||||
|
|
||||||
exclude-rules:
|
|
||||||
# If future reflect.Kinds are nil-able, we'll find out when a test fails.
|
|
||||||
- linters: [exhaustive]
|
|
||||||
path: internal/assert/assert.go
|
|
||||||
# We need our duplex HTTP call to have access to the context.
|
|
||||||
- linters: [containedctx]
|
|
||||||
path: duplex_http_call.go
|
|
||||||
# We need to init a global in-mem HTTP server for testable examples.
|
|
||||||
- linters: [gochecknoinits, gochecknoglobals]
|
|
||||||
path: example_init_test.go
|
|
||||||
# We purposefully do an ineffectual assignment for an example.
|
|
||||||
- linters: [ineffassign]
|
|
||||||
path: client_example_test.go
|
|
||||||
# The generated file is effectively a global receiver.
|
|
||||||
- linters: [varnamelen]
|
|
||||||
path: cmd/protoc-gen-connect-go
|
|
||||||
text: "parameter name 'g' is too short"
|
|
||||||
# Thorough error logging and timeout config make this example unreadably long.
|
|
||||||
- linters: [errcheck, gosec]
|
|
||||||
path: error_writer_example_test.go
|
|
||||||
# It should be crystal clear that Connect uses plain *http.Clients.
|
|
||||||
- linters: [revive, stylecheck]
|
|
||||||
path: client_example_test.go
|
|
||||||
# Don't complain about timeout management or lack of output assertions in examples.
|
|
||||||
- linters: [gosec, testableexamples]
|
|
||||||
path: handler_example_test.go
|
|
||||||
# No output assertions needed for these examples.
|
|
||||||
- linters: [testableexamples]
|
|
||||||
path: error_writer_example_test.go
|
|
||||||
- linters: [testableexamples]
|
|
||||||
path: error_not_modified_example_test.go
|
|
||||||
- linters: [testableexamples]
|
|
||||||
path: error_example_test.go
|
|
||||||
# In examples, it's okay to use http.ListenAndServe.
|
|
||||||
- linters: [gosec]
|
|
||||||
path: error_not_modified_example_test.go
|
|
||||||
# There are many instances where we want to keep unused parameters
|
|
||||||
# as a matter of style or convention, for example when a context.Context
|
|
||||||
# is the first parameter, we choose to just globally ignore this.
|
|
||||||
- linters: [revive]
|
|
||||||
text: "^unused-parameter: "
|
|
||||||
# We want to return explicit nils in protocol_grpc.go
|
|
||||||
- linters: [revive]
|
|
||||||
text: "^if-return: "
|
|
||||||
path: protocol_grpc.go
|
|
||||||
# We want to return explicit nils in protocol_connect.go
|
|
||||||
- linters: [revive]
|
|
||||||
text: "^if-return: "
|
|
||||||
path: protocol_connect.go
|
|
||||||
# We want to return explicit nils in error_writer.go
|
|
||||||
- linters: [revive]
|
|
||||||
text: "^if-return: "
|
|
||||||
path: error_writer.go
|
|
||||||
# We want to set http.Server's logger
|
|
||||||
- linters: [forbidigo]
|
|
||||||
path: internal/memhttp
|
|
||||||
text: "use of `log.(New|Logger|Lshortfile)` forbidden by pattern .*"
|
|
||||||
# We want to show examples with http.Get
|
|
||||||
- linters: [noctx]
|
|
||||||
path: internal/memhttp/memhttp_test.go
|
|
||||||
# Allow fmt.Sprintf for cmd/protoc-gen-connect-go for consistency
|
|
||||||
- linters: [perfsprint]
|
|
||||||
path: cmd/protoc-gen-connect-go/main.go
|
|
||||||
# Allow non-canonical headers in tests
|
|
||||||
- linters: [canonicalheader]
|
|
||||||
path: '.*_test.go'
|
|
||||||
# Allow Code pointer receiver for UnmarshalText method
|
|
||||||
- linters: [recvcheck]
|
|
||||||
path: code.go
|
|
||||||
# Avoid false positives for int overflow in tests
|
|
||||||
- linters: [gosec]
|
|
||||||
text: "^G115: integer overflow conversion"
|
|
||||||
path: ".*_test.go"
|
|
||||||
-201
@@ -1,201 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2021-2025 The Connect Authors
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
Maintainers
|
|
||||||
===========
|
|
||||||
|
|
||||||
## Current
|
|
||||||
* [Peter Edge](https://github.com/bufdev), [Buf](https://buf.build)
|
|
||||||
* [Josh Humphries](https://github.com/jhump), [Buf](https://buf.build)
|
|
||||||
* [Matt Robenolt](https://github.com/mattrobenolt), [PlanetScale](https://planetscale.com)
|
|
||||||
* [Edward McFarlane](https://github.com/emcfarlane), [Buf](https://buf.build)
|
|
||||||
|
|
||||||
## Former
|
|
||||||
* [Akshay Shah](https://github.com/akshayjshah)
|
|
||||||
* [Alex McKinney](https://github.com/amckinney)
|
|
||||||
-121
@@ -1,121 +0,0 @@
|
|||||||
# See https://tech.davis-hansson.com/p/make/
|
|
||||||
SHELL := bash
|
|
||||||
.DELETE_ON_ERROR:
|
|
||||||
.SHELLFLAGS := -eu -o pipefail -c
|
|
||||||
.DEFAULT_GOAL := all
|
|
||||||
MAKEFLAGS += --warn-undefined-variables
|
|
||||||
MAKEFLAGS += --no-builtin-rules
|
|
||||||
MAKEFLAGS += --no-print-directory
|
|
||||||
BIN := .tmp/bin
|
|
||||||
export PATH := $(abspath $(BIN)):$(PATH)
|
|
||||||
export GOBIN := $(abspath $(BIN))
|
|
||||||
COPYRIGHT_YEARS := 2021-2025
|
|
||||||
LICENSE_IGNORE := --ignore /testdata/
|
|
||||||
BUF_VERSION := 1.50.1
|
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help: ## Describe useful make targets
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: ## Build, test, and lint (default)
|
|
||||||
$(MAKE) test
|
|
||||||
$(MAKE) lint
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean: ## Delete intermediate build artifacts
|
|
||||||
@# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs
|
|
||||||
git clean -Xdf
|
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test: shorttest slowtest
|
|
||||||
|
|
||||||
.PHONY: shorttest
|
|
||||||
shorttest: build ## Run unit tests
|
|
||||||
go test -vet=off -race -cover -short ./...
|
|
||||||
|
|
||||||
.PHONY: slowtest
|
|
||||||
# Runs all tests, including known long/slow ones. The
|
|
||||||
# race detector is not used for a few reasons:
|
|
||||||
# 1. Race coverage of the short tests should be
|
|
||||||
# adequate to catch race conditions.
|
|
||||||
# 2. It slows tests down, which is not good if we
|
|
||||||
# know these are already slow tests.
|
|
||||||
# 3. Some of the slow tests can't repro issues and
|
|
||||||
# find regressions as reliably with the race
|
|
||||||
# detector enabled.
|
|
||||||
slowtest: build
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
.PHONY: runconformance
|
|
||||||
runconformance: build ## Run conformance test suite
|
|
||||||
cd internal/conformance && ./runconformance.sh
|
|
||||||
|
|
||||||
.PHONY: bench
|
|
||||||
bench: BENCH ?= .*
|
|
||||||
bench: build ## Run benchmarks for root package
|
|
||||||
go test -vet=off -run '^$$' -bench '$(BENCH)' -benchmem -cpuprofile cpu.pprof -memprofile mem.pprof .
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build: generate ## Build all packages
|
|
||||||
go build ./...
|
|
||||||
|
|
||||||
.PHONY: install
|
|
||||||
install: ## Install all binaries
|
|
||||||
go install ./...
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint: $(BIN)/golangci-lint $(BIN)/buf ## Lint Go and protobuf
|
|
||||||
go vet ./...
|
|
||||||
golangci-lint run --modules-download-mode=readonly --timeout=3m0s
|
|
||||||
buf lint
|
|
||||||
buf format -d --exit-code
|
|
||||||
|
|
||||||
.PHONY: lintfix
|
|
||||||
lintfix: $(BIN)/golangci-lint $(BIN)/buf ## Automatically fix some lint errors
|
|
||||||
golangci-lint run --fix --modules-download-mode=readonly --timeout=3m0s
|
|
||||||
buf format -w
|
|
||||||
|
|
||||||
.PHONY: generate
|
|
||||||
generate: $(BIN)/buf $(BIN)/protoc-gen-go $(BIN)/protoc-gen-connect-go $(BIN)/license-header ## Regenerate code and licenses
|
|
||||||
go mod tidy
|
|
||||||
cd ./internal/conformance && go mod tidy
|
|
||||||
buf generate
|
|
||||||
cd ./cmd/protoc-gen-connect-go/internal && \
|
|
||||||
find ./testdata -maxdepth 1 -type d \( ! -name testdata \) | xargs -n 1 -I % bash -c "cd '%' && buf generate"
|
|
||||||
license-header \
|
|
||||||
--license-type apache \
|
|
||||||
--copyright-holder "The Connect Authors" \
|
|
||||||
--year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE)
|
|
||||||
|
|
||||||
.PHONY: upgrade
|
|
||||||
upgrade: ## Upgrade dependencies
|
|
||||||
go get -u -t ./... && go mod tidy -v
|
|
||||||
|
|
||||||
.PHONY: checkgenerate
|
|
||||||
checkgenerate:
|
|
||||||
@# Used in CI to verify that `make generate` doesn't produce a diff.
|
|
||||||
test -z "$$(git status --porcelain | tee /dev/stderr)"
|
|
||||||
|
|
||||||
.PHONY: $(BIN)/protoc-gen-connect-go
|
|
||||||
$(BIN)/protoc-gen-connect-go:
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
go build -o $(@) ./cmd/protoc-gen-connect-go
|
|
||||||
|
|
||||||
$(BIN)/buf: Makefile
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
go install github.com/bufbuild/buf/cmd/buf@v${BUF_VERSION}
|
|
||||||
|
|
||||||
$(BIN)/license-header: Makefile
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v${BUF_VERSION}
|
|
||||||
|
|
||||||
$(BIN)/golangci-lint: Makefile
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.7
|
|
||||||
|
|
||||||
$(BIN)/protoc-gen-go: Makefile go.mod
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
@# The version of protoc-gen-go is determined by the version in go.mod
|
|
||||||
go install google.golang.org/protobuf/cmd/protoc-gen-go
|
|
||||||
|
|
||||||
-184
@@ -1,184 +0,0 @@
|
|||||||
Connect
|
|
||||||
=======
|
|
||||||
|
|
||||||
[](https://github.com/connectrpc/connect-go/actions/workflows/ci.yaml)
|
|
||||||
[](https://goreportcard.com/report/connectrpc.com/connect)
|
|
||||||
[](https://pkg.go.dev/connectrpc.com/connect)
|
|
||||||
[][slack]
|
|
||||||
[](https://www.bestpractices.dev/projects/8972)
|
|
||||||
|
|
||||||
Connect is a slim library for building browser and gRPC-compatible HTTP APIs.
|
|
||||||
You write a short [Protocol Buffer][protobuf] schema and implement your
|
|
||||||
application logic, and Connect generates code to handle marshaling, routing,
|
|
||||||
compression, and content type negotiation. It also generates an idiomatic,
|
|
||||||
type-safe client. Handlers and clients support three protocols: gRPC, gRPC-Web,
|
|
||||||
and Connect's own protocol.
|
|
||||||
|
|
||||||
The [Connect protocol][protocol] is a simple protocol that works over HTTP/1.1
|
|
||||||
or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including
|
|
||||||
streaming, and packages them into a protocol that works equally well in
|
|
||||||
browsers, monoliths, and microservices. Calling a Connect API is as easy as
|
|
||||||
using `curl`. Try it with our live demo:
|
|
||||||
|
|
||||||
```
|
|
||||||
curl \
|
|
||||||
--header "Content-Type: application/json" \
|
|
||||||
--data '{"sentence": "I feel happy."}' \
|
|
||||||
https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say
|
|
||||||
```
|
|
||||||
|
|
||||||
Handlers and clients also support the gRPC and gRPC-Web protocols, including
|
|
||||||
streaming, headers, trailers, and error details. gRPC-compatible [server
|
|
||||||
reflection][grpcreflect] and [health checks][grpchealth] are available as
|
|
||||||
standalone packages. Instead of cURL, we could call our API with a gRPC client:
|
|
||||||
|
|
||||||
```
|
|
||||||
go install github.com/bufbuild/buf/cmd/buf@latest
|
|
||||||
buf curl --protocol grpc \
|
|
||||||
--data '{"sentence": "I feel happy."}' \
|
|
||||||
https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say
|
|
||||||
```
|
|
||||||
|
|
||||||
Under the hood, Connect is just [Protocol Buffers][protobuf] and the standard
|
|
||||||
library: no custom HTTP implementation, no new name resolution or load
|
|
||||||
balancing APIs, and no surprises. Everything you already know about `net/http`
|
|
||||||
still applies, and any package that works with an `http.Server`, `http.Client`,
|
|
||||||
or `http.Handler` also works with Connect.
|
|
||||||
|
|
||||||
For more on Connect, see the [announcement blog post][blog], the documentation
|
|
||||||
on [connectrpc.com][docs] (especially the [Getting Started] guide for Go), the
|
|
||||||
[demo service][examples-go], or the [protocol specification][protocol].
|
|
||||||
|
|
||||||
## A small example
|
|
||||||
|
|
||||||
Curious what all this looks like in practice? From a [Protobuf
|
|
||||||
schema](internal/proto/connect/ping/v1/ping.proto), we generate [a small RPC
|
|
||||||
package](internal/gen/simple/connect/ping/v1/pingv1connect/ping.connect.go). Using that
|
|
||||||
package, we can build a server. This example is available at [internal/example](internal/example):
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
|
||||||
pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1"
|
|
||||||
"connectrpc.com/connect/internal/gen/simple/connect/ping/v1/pingv1connect"
|
|
||||||
"connectrpc.com/validate"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PingServer struct {
|
|
||||||
pingv1connect.UnimplementedPingServiceHandler // returns errors from all methods
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps *PingServer) Ping(ctx context.Context, req *pingv1.PingRequest) (*pingv1.PingResponse, error) {
|
|
||||||
return &pingv1.PingResponse{
|
|
||||||
Number: req.Number,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
// The generated constructors return a path and a plain net/http
|
|
||||||
// handler.
|
|
||||||
mux.Handle(
|
|
||||||
pingv1connect.NewPingServiceHandler(
|
|
||||||
&PingServer{},
|
|
||||||
// Validation via Protovalidate is almost always recommended
|
|
||||||
connect.WithInterceptors(validate.NewInterceptor()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
p := new(http.Protocols)
|
|
||||||
p.SetHTTP1(true)
|
|
||||||
// For gRPC clients, it's convenient to support HTTP/2 without TLS.
|
|
||||||
p.SetUnencryptedHTTP2(true)
|
|
||||||
s := &http.Server{
|
|
||||||
Addr: "localhost:8080",
|
|
||||||
Handler: mux,
|
|
||||||
Protocols: p,
|
|
||||||
}
|
|
||||||
if err := s.ListenAndServe(); err != nil {
|
|
||||||
log.Fatalf("listen failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With that server running, you can make requests with any gRPC or Connect
|
|
||||||
client. To write a client using Connect:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1"
|
|
||||||
"connectrpc.com/connect/internal/gen/simple/connect/ping/v1/pingv1connect"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
client := pingv1connect.NewPingServiceClient(
|
|
||||||
http.DefaultClient,
|
|
||||||
"http://localhost:8080/",
|
|
||||||
)
|
|
||||||
req := &pingv1.PingRequest{Number: 42}
|
|
||||||
res, err := client.Ping(context.Background(), req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
log.Println(res)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Of course, `http.ListenAndServe` and `http.DefaultClient` aren't fit for
|
|
||||||
production use! See Connect's [deployment docs][docs-deployment] for a guide to
|
|
||||||
configuring timeouts, connection pools, observability, and h2c.
|
|
||||||
|
|
||||||
## Ecosystem
|
|
||||||
|
|
||||||
* [grpchealth]: gRPC-compatible health checks for connect-go
|
|
||||||
* [grpcreflect]: gRPC-compatible server reflection for connect-go
|
|
||||||
* [validate]: [Protovalidate][protovalidate] interceptor for connect-go
|
|
||||||
* [examples-go]: service powering [demo.connectrpc.com](https://demo.connectrpc.com), including bidi streaming
|
|
||||||
* [connect-es]: Type-safe APIs with Protobuf and TypeScript
|
|
||||||
* [Buf Studio]: web UI for ad-hoc RPCs
|
|
||||||
* [conformance]: Connect, gRPC, and gRPC-Web interoperability tests
|
|
||||||
|
|
||||||
## Status: Stable
|
|
||||||
|
|
||||||
This module is stable. It supports:
|
|
||||||
|
|
||||||
* The two most recent major releases of Go (the same versions of Go that continue
|
|
||||||
to [receive security patches][go-support-policy]).
|
|
||||||
* [APIv2] of Protocol Buffers in Go (`google.golang.org/protobuf`).
|
|
||||||
|
|
||||||
Within those parameters, `connect` follows semantic versioning. We will
|
|
||||||
_not_ make breaking changes in the 1.x series of releases.
|
|
||||||
|
|
||||||
## Legal
|
|
||||||
|
|
||||||
Offered under the [Apache 2 license][license].
|
|
||||||
|
|
||||||
[APIv2]: https://blog.golang.org/protobuf-apiv2
|
|
||||||
[Buf Studio]: https://buf.build/studio
|
|
||||||
[Getting Started]: https://connectrpc.com/docs/go/getting-started
|
|
||||||
[blog]: https://buf.build/blog/connect-a-better-grpc
|
|
||||||
[conformance]: https://github.com/connectrpc/conformance
|
|
||||||
[grpchealth]: https://github.com/connectrpc/grpchealth-go
|
|
||||||
[grpcreflect]: https://github.com/connectrpc/grpcreflect-go
|
|
||||||
[connect-es]: https://github.com/connectrpc/connect-es
|
|
||||||
[examples-go]: https://github.com/connectrpc/examples-go
|
|
||||||
[docs-deployment]: https://connectrpc.com/docs/go/deployment
|
|
||||||
[docs]: https://connectrpc.com
|
|
||||||
[go-support-policy]: https://golang.org/doc/devel/release#policy
|
|
||||||
[license]: https://github.com/connectrpc/connect-go/blob/main/LICENSE
|
|
||||||
[protobuf]: https://developers.google.com/protocol-buffers
|
|
||||||
[protocol]: https://connectrpc.com/docs/protocol
|
|
||||||
[slack]: https://buf.build/links/slack
|
|
||||||
[validate]: https://github.com/connectrpc/validate-go
|
|
||||||
[protovalidate]: https://protovalidate.com
|
|
||||||
-44
@@ -1,44 +0,0 @@
|
|||||||
# Releasing connect-go
|
|
||||||
|
|
||||||
This document outlines how to create a release of connect-go.
|
|
||||||
|
|
||||||
1. Clone the repo, ensuring you have the latest main.
|
|
||||||
|
|
||||||
2. On a new branch, open [connect.go](connect.go) and change the `Version` constant to an appropriate [semantic version](https://semver.org/). To select the correct version, look at the version number of the [latest release] and the changes that are included in this new release.
|
|
||||||
* If there are only bug fixes and no new features, remove the `-dev` suffix, set MINOR number to be equal to the [latest release], and set the PATCH number to be 1 more than the PATCH number of the [latest release].
|
|
||||||
* If there are features being released, remove the `-dev` suffix, set the MINOR number to be 1 more than the MINOR number of the [latest release], and set the PATCH number to `0`. In the common case, the diff here will just be to remove the `-dev` suffix.
|
|
||||||
|
|
||||||
```patch
|
|
||||||
-const Version = "1.14.0-dev"
|
|
||||||
+const Version = "1.14.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Check for any changes in [cmd/protoc-gen-connect-go/main.go](cmd/protoc-gen-connect-go/main.go) that require a version restriction. A constant `IsAtLeastVersionX_Y_Z` should be defined in [connect.go](connect.go) if generated code has begun to use a new API. Make sure the generated code references this constant. If a new constant has been added since the last release, ensure that the name of the constant matches the version being released ([Example PR #496](https://github.com/connectrpc/connect-go/pull/496)).
|
|
||||||
|
|
||||||
4. Open a PR titled "Prepare for vX.Y.Z" ([Example PR #661](https://github.com/connectrpc/connect-go/pull/661)) and a description tagging all current maintainers. Once it's reviewed and CI passes, merge it.
|
|
||||||
|
|
||||||
*Make sure no new commits are merged until the release is complete.*
|
|
||||||
|
|
||||||
5. Review all commits in the new release and for each PR check an appropriate label is used and edit the title to be meaninful to end users. This will help auto-generated release notes match the final notes as closely as possible.
|
|
||||||
|
|
||||||
6. Using the Github UI, create a new release.
|
|
||||||
- Under “Choose a tag”, type in “vX.Y.Z” to create a new tag for the release upon publish.
|
|
||||||
- Target the main branch.
|
|
||||||
- Title the Release “vX.Y.Z”.
|
|
||||||
- Click “set as latest release”.
|
|
||||||
- Set the last version as the “Previous tag”.
|
|
||||||
- Click “Generate release notes” to autogenerate release notes.
|
|
||||||
- Edit the release notes. A summary and other sub categories may be added if required but should, in most cases, be left as ### Enhancements and ### Bugfixes. Feel free to collect multiple small changes to docs or Github config into one line, but try to tag every contributor. Make especially sure to credit new external contributors!
|
|
||||||
|
|
||||||
7. Publish the release.
|
|
||||||
|
|
||||||
8. On a new branch, open [connect.go](connect.go) and change the `Version` to increment the minor tag and append the `-dev` suffix. Use the next minor release - we never anticipate bugs and patch releases.
|
|
||||||
|
|
||||||
```patch
|
|
||||||
-const Version = "1.14.0"
|
|
||||||
+const Version = "1.15.0-dev"
|
|
||||||
```
|
|
||||||
|
|
||||||
9. Open a PR titled "Back to development" ([Example PR #662](https://github.com/connectrpc/connect-go/pull/662)). Once it's reviewed and CI passes, merge it.
|
|
||||||
|
|
||||||
[latest release]: https://github.com/connectrpc/connect-go/releases/latest
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
Security Policy
|
|
||||||
===============
|
|
||||||
|
|
||||||
This project follows the [Connect security policy and reporting
|
|
||||||
process](https://connectrpc.com/docs/governance/security).
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
version: v2
|
|
||||||
managed:
|
|
||||||
enabled: true
|
|
||||||
override:
|
|
||||||
- file_option: go_package_prefix
|
|
||||||
value: connectrpc.com/connect/internal/gen
|
|
||||||
plugins:
|
|
||||||
- local: protoc-gen-go
|
|
||||||
out: internal/gen
|
|
||||||
opt: paths=source_relative
|
|
||||||
- local: protoc-gen-connect-go
|
|
||||||
out: internal/gen/generics
|
|
||||||
opt: paths=source_relative
|
|
||||||
- local: protoc-gen-connect-go
|
|
||||||
out: internal/gen/simple
|
|
||||||
opt:
|
|
||||||
- paths=source_relative
|
|
||||||
- simple
|
|
||||||
clean: true
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
version: v2
|
|
||||||
modules:
|
|
||||||
- path: internal/proto
|
|
||||||
lint:
|
|
||||||
use:
|
|
||||||
- STANDARD
|
|
||||||
ignore:
|
|
||||||
- internal/proto/connectext/grpc/health/v1/health.proto
|
|
||||||
- internal/proto/connectext/grpc/reflection/v1alpha/reflection.proto
|
|
||||||
- internal/proto/connectext/grpc/status/v1/status.proto
|
|
||||||
disallow_comment_ignores: true
|
|
||||||
breaking:
|
|
||||||
use:
|
|
||||||
- WIRE_JSON
|
|
||||||
-54
@@ -1,54 +0,0 @@
|
|||||||
// Copyright 2021-2025 The Connect Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
initialBufferSize = 512
|
|
||||||
maxRecycleBufferSize = 8 * 1024 * 1024 // if >8MiB, don't hold onto a buffer
|
|
||||||
)
|
|
||||||
|
|
||||||
type bufferPool struct {
|
|
||||||
sync.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBufferPool() *bufferPool {
|
|
||||||
return &bufferPool{
|
|
||||||
Pool: sync.Pool{
|
|
||||||
New: func() any {
|
|
||||||
return bytes.NewBuffer(make([]byte, 0, initialBufferSize))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bufferPool) Get() *bytes.Buffer {
|
|
||||||
if buf, ok := b.Pool.Get().(*bytes.Buffer); ok {
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
return bytes.NewBuffer(make([]byte, 0, initialBufferSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bufferPool) Put(buffer *bytes.Buffer) {
|
|
||||||
if buffer.Cap() > maxRecycleBufferSize {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buffer.Reset()
|
|
||||||
b.Pool.Put(buffer)
|
|
||||||
}
|
|
||||||
-392
@@ -1,392 +0,0 @@
|
|||||||
// Copyright 2021-2025 The Connect Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client is a reusable, concurrency-safe client for a single procedure.
|
|
||||||
// Depending on the procedure's type, use the CallUnary, CallClientStream,
|
|
||||||
// CallServerStream, or CallBidiStream method.
|
|
||||||
//
|
|
||||||
// By default, clients use the Connect protocol with the binary Protobuf Codec,
|
|
||||||
// ask for gzipped responses, and send uncompressed requests. To use the gRPC
|
|
||||||
// or gRPC-Web protocols, use the [WithGRPC] or [WithGRPCWeb] options.
|
|
||||||
type Client[Req, Res any] struct {
|
|
||||||
config *clientConfig
|
|
||||||
callUnary func(context.Context, *Request[Req]) (*Response[Res], error)
|
|
||||||
protocolClient protocolClient
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient constructs a new Client.
|
|
||||||
func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...ClientOption) *Client[Req, Res] {
|
|
||||||
client := &Client[Req, Res]{}
|
|
||||||
config, err := newClientConfig(url, options)
|
|
||||||
if err != nil {
|
|
||||||
client.err = err
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
client.config = config
|
|
||||||
protocolClient, protocolErr := client.config.Protocol.NewClient(
|
|
||||||
&protocolClientParams{
|
|
||||||
CompressionName: config.RequestCompressionName,
|
|
||||||
CompressionPools: newReadOnlyCompressionPools(
|
|
||||||
config.CompressionPools,
|
|
||||||
config.CompressionNames,
|
|
||||||
),
|
|
||||||
Codec: config.Codec,
|
|
||||||
Protobuf: config.protobuf(),
|
|
||||||
CompressMinBytes: config.CompressMinBytes,
|
|
||||||
HTTPClient: httpClient,
|
|
||||||
URL: config.URL,
|
|
||||||
BufferPool: config.BufferPool,
|
|
||||||
ReadMaxBytes: config.ReadMaxBytes,
|
|
||||||
SendMaxBytes: config.SendMaxBytes,
|
|
||||||
EnableGet: config.EnableGet,
|
|
||||||
GetURLMaxBytes: config.GetURLMaxBytes,
|
|
||||||
GetUseFallback: config.GetUseFallback,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if protocolErr != nil {
|
|
||||||
client.err = protocolErr
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
client.protocolClient = protocolClient
|
|
||||||
// Rather than applying unary interceptors along the hot path, we can do it
|
|
||||||
// once at client creation.
|
|
||||||
unarySpec := config.newSpec(StreamTypeUnary)
|
|
||||||
unaryFunc := UnaryFunc(func(ctx context.Context, request AnyRequest) (AnyResponse, error) {
|
|
||||||
conn := client.protocolClient.NewConn(ctx, unarySpec, request.Header())
|
|
||||||
conn.onRequestSend(func(r *http.Request) {
|
|
||||||
request.setRequestMethod(r.Method)
|
|
||||||
callInfo, ok := clientCallInfoForContext(ctx)
|
|
||||||
if ok {
|
|
||||||
callInfo.method = r.Method
|
|
||||||
callInfo.responseSource = conn
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Send always returns an io.EOF unless the error is from the client-side.
|
|
||||||
// We want the user to continue to call Receive in those cases to get the
|
|
||||||
// full error from the server-side.
|
|
||||||
if err := conn.Send(request.Any()); err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
_ = conn.CloseRequest()
|
|
||||||
_ = conn.CloseResponse()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.CloseRequest(); err != nil {
|
|
||||||
_ = conn.CloseResponse()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response, err := receiveUnaryResponse[Res](conn, config.Initializer)
|
|
||||||
if err != nil {
|
|
||||||
_ = conn.CloseResponse()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return response, conn.CloseResponse()
|
|
||||||
})
|
|
||||||
if interceptor := config.Interceptor; interceptor != nil {
|
|
||||||
// interceptor is the full chain of all interceptors provided
|
|
||||||
unaryFunc = interceptor.WrapUnary(unaryFunc)
|
|
||||||
}
|
|
||||||
client.callUnary = func(ctx context.Context, request *Request[Req]) (*Response[Res], error) {
|
|
||||||
// To make the specification, peer, and RPC headers visible to the full
|
|
||||||
// interceptor chain (as though they were supplied by the caller), we'll
|
|
||||||
// add them here.
|
|
||||||
request.spec = unarySpec
|
|
||||||
request.peer = client.protocolClient.Peer()
|
|
||||||
protocolClient.WriteRequestHeader(StreamTypeUnary, request.Header())
|
|
||||||
|
|
||||||
// Also set them in the context if there's a call info present
|
|
||||||
callInfo, callInfoOk := clientCallInfoForContext(ctx)
|
|
||||||
if callInfoOk {
|
|
||||||
callInfo.peer = request.Peer()
|
|
||||||
callInfo.spec = request.Spec()
|
|
||||||
// A client could have set request headers in the call info OR the request wrapper
|
|
||||||
// So if a callInfo exists in context, merge any headers from there into the request wrapper
|
|
||||||
// so that all headers are sent in the request
|
|
||||||
mergeHeaders(request.Header(), callInfo.requestHeader)
|
|
||||||
|
|
||||||
// Copy the call info into a sentinel value. This is so we can compare
|
|
||||||
// the sentinel value against the call info in context. If they're different,
|
|
||||||
// we can stop the request. This protects against changing the context in interceptors.
|
|
||||||
ctx = context.WithValue(ctx, sentinelContextKey{}, callInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := unaryFunc(ctx, request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
typed, ok := response.(*Response[Res])
|
|
||||||
if !ok {
|
|
||||||
return nil, errorf(CodeInternal, "unexpected client response type %T", response)
|
|
||||||
}
|
|
||||||
return typed, nil
|
|
||||||
}
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallUnary calls a request-response procedure.
|
|
||||||
func (c *Client[Req, Res]) CallUnary(ctx context.Context, request *Request[Req]) (*Response[Res], error) {
|
|
||||||
if c.err != nil {
|
|
||||||
return nil, c.err
|
|
||||||
}
|
|
||||||
return c.callUnary(ctx, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallClientStream calls a client streaming procedure.
|
|
||||||
//
|
|
||||||
// Request headers can be sent via the [ClientStreamForClient.RequestHeader] method on the stream. Note that the
|
|
||||||
// request headers are not sent automatically when this method is invoked and instead require an explicit call to
|
|
||||||
// [ClientStreamForClient.Send].
|
|
||||||
func (c *Client[Req, Res]) CallClientStream(ctx context.Context) *ClientStreamForClient[Req, Res] {
|
|
||||||
if c.err != nil {
|
|
||||||
return &ClientStreamForClient[Req, Res]{err: c.err}
|
|
||||||
}
|
|
||||||
return &ClientStreamForClient[Req, Res]{
|
|
||||||
conn: c.newConn(ctx, StreamTypeClient, nil),
|
|
||||||
initializer: c.config.Initializer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallClientStreamSimple calls a client streaming procedure.
|
|
||||||
//
|
|
||||||
// Request headers should be set in a [CallInfo] object inside the context using [NewClientContext]. These headers are
|
|
||||||
// transmitted when this method is called and do not require an explicit call to [ClientStreamForClientSimple.Send].
|
|
||||||
//
|
|
||||||
// In addition, when calling [ClientStreamForClientSimple.CloseAndReceive] on the returned stream, the returned response
|
|
||||||
// is the response type defined for the stream and _not_ a Connect [Response] wrapper type. As a result, any response
|
|
||||||
// headers and trailers should be read from the [CallInfo] object in context.
|
|
||||||
func (c *Client[Req, Res]) CallClientStreamSimple(ctx context.Context) (*ClientStreamForClientSimple[Req, Res], error) {
|
|
||||||
if c.err != nil {
|
|
||||||
return &ClientStreamForClientSimple[Req, Res]{
|
|
||||||
stream: &ClientStreamForClient[Req, Res]{err: c.err},
|
|
||||||
}, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
stream := &ClientStreamForClientSimple[Req, Res]{
|
|
||||||
stream: &ClientStreamForClient[Req, Res]{
|
|
||||||
conn: c.newConn(ctx, StreamTypeClient, nil),
|
|
||||||
initializer: c.config.Initializer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := stream.Send(nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallServerStream calls a server streaming procedure.
|
|
||||||
func (c *Client[Req, Res]) CallServerStream(ctx context.Context, request *Request[Req]) (*ServerStreamForClient[Res], error) {
|
|
||||||
if c.err != nil {
|
|
||||||
return nil, c.err
|
|
||||||
}
|
|
||||||
conn := c.newConn(ctx, StreamTypeServer, func(r *http.Request) {
|
|
||||||
request.method = r.Method
|
|
||||||
})
|
|
||||||
request.peer = conn.Peer()
|
|
||||||
request.spec = conn.Spec()
|
|
||||||
|
|
||||||
mergeHeaders(conn.RequestHeader(), request.header)
|
|
||||||
|
|
||||||
// Send always returns an io.EOF unless the error is from the client-side.
|
|
||||||
// We want the user to continue to call Receive in those cases to get the
|
|
||||||
// full error from the server-side.
|
|
||||||
if err := conn.Send(request.Msg); err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
_ = conn.CloseRequest()
|
|
||||||
_ = conn.CloseResponse()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.CloseRequest(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ServerStreamForClient[Res]{
|
|
||||||
conn: conn,
|
|
||||||
initializer: c.config.Initializer,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallBidiStream calls a bidirectional streaming procedure.
|
|
||||||
//
|
|
||||||
// Request headers can be sent via the [BidiStreamForClient.RequestHeader] method. Note that the
|
|
||||||
// request headers are not sent automatically when this method is invoked and instead require an explicit call to
|
|
||||||
// [BidiStreamForClient.Send].
|
|
||||||
func (c *Client[Req, Res]) CallBidiStream(ctx context.Context) *BidiStreamForClient[Req, Res] {
|
|
||||||
if c.err != nil {
|
|
||||||
return &BidiStreamForClient[Req, Res]{err: c.err}
|
|
||||||
}
|
|
||||||
return &BidiStreamForClient[Req, Res]{
|
|
||||||
conn: c.newConn(ctx, StreamTypeBidi, nil),
|
|
||||||
initializer: c.config.Initializer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallBidiStreamSimple calls a bidirectional streaming procedure.
|
|
||||||
//
|
|
||||||
// Request headers should be set in a [CallInfo] object inside the context using [NewClientContext]. These headers
|
|
||||||
// are transmitted when this method is called and do not require an explicit call to [BidiStreamForClient.Send].
|
|
||||||
//
|
|
||||||
// Likewise, response headers and trailers should be read from the [CallInfo] object in context.
|
|
||||||
func (c *Client[Req, Res]) CallBidiStreamSimple(ctx context.Context) (*BidiStreamForClientSimple[Req, Res], error) {
|
|
||||||
if c.err != nil {
|
|
||||||
return &BidiStreamForClientSimple[Req, Res]{
|
|
||||||
stream: &BidiStreamForClient[Req, Res]{err: c.err},
|
|
||||||
}, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
stream := &BidiStreamForClientSimple[Req, Res]{
|
|
||||||
stream: &BidiStreamForClient[Req, Res]{
|
|
||||||
conn: c.newConn(ctx, StreamTypeBidi, nil),
|
|
||||||
initializer: c.config.Initializer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := stream.Send(nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client[Req, Res]) newConn(ctx context.Context, streamType StreamType, onRequestSend func(r *http.Request)) StreamingClientConn {
|
|
||||||
callInfo, callInfoOk := clientCallInfoForContext(ctx)
|
|
||||||
// Set values in the context if there's a call info present
|
|
||||||
if callInfoOk {
|
|
||||||
// Copy the call info into a sentinel value. This is so we can compare
|
|
||||||
// the sentinel value against the call info in context. If they're different,
|
|
||||||
// we can stop the request. This protects against changing the context in interceptors.
|
|
||||||
ctx = context.WithValue(ctx, sentinelContextKey{}, callInfo)
|
|
||||||
}
|
|
||||||
newConn := func(ctx context.Context, spec Spec) StreamingClientConn {
|
|
||||||
header := make(http.Header, 8) // arbitrary power of two, prevent immediate resizing
|
|
||||||
c.protocolClient.WriteRequestHeader(streamType, header)
|
|
||||||
conn := c.protocolClient.NewConn(ctx, spec, header)
|
|
||||||
conn.onRequestSend(onRequestSend)
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
if interceptor := c.config.Interceptor; interceptor != nil {
|
|
||||||
newConn = interceptor.WrapStreamingClient(newConn)
|
|
||||||
}
|
|
||||||
conn := newConn(ctx, c.config.newSpec(streamType))
|
|
||||||
|
|
||||||
// Set values in the context if there's a call info present
|
|
||||||
if callInfoOk {
|
|
||||||
callInfo.peer = conn.Peer()
|
|
||||||
callInfo.spec = conn.Spec()
|
|
||||||
callInfo.responseSource = conn
|
|
||||||
|
|
||||||
// Merge any callInfo request headers first, then do the request,
|
|
||||||
// so that context headers show first in the list of headers.
|
|
||||||
mergeHeaders(conn.RequestHeader(), callInfo.RequestHeader())
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientConfig struct {
|
|
||||||
URL *url.URL
|
|
||||||
Protocol protocol
|
|
||||||
Procedure string
|
|
||||||
Schema any
|
|
||||||
Initializer maybeInitializer
|
|
||||||
CompressMinBytes int
|
|
||||||
Interceptor Interceptor
|
|
||||||
CompressionPools map[string]*compressionPool
|
|
||||||
CompressionNames []string
|
|
||||||
Codec Codec
|
|
||||||
RequestCompressionName string
|
|
||||||
BufferPool *bufferPool
|
|
||||||
ReadMaxBytes int
|
|
||||||
SendMaxBytes int
|
|
||||||
EnableGet bool
|
|
||||||
GetURLMaxBytes int
|
|
||||||
GetUseFallback bool
|
|
||||||
IdempotencyLevel IdempotencyLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClientConfig(rawURL string, options []ClientOption) (*clientConfig, *Error) {
|
|
||||||
url, err := parseRequestURL(rawURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
protoPath := extractProtoPath(url.Path)
|
|
||||||
config := clientConfig{
|
|
||||||
URL: url,
|
|
||||||
Protocol: &protocolConnect{},
|
|
||||||
Procedure: protoPath,
|
|
||||||
CompressionPools: make(map[string]*compressionPool),
|
|
||||||
BufferPool: newBufferPool(),
|
|
||||||
}
|
|
||||||
withProtoBinaryCodec().applyToClient(&config)
|
|
||||||
withGzip().applyToClient(&config)
|
|
||||||
for _, opt := range options {
|
|
||||||
opt.applyToClient(&config)
|
|
||||||
}
|
|
||||||
if err := config.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientConfig) validate() *Error {
|
|
||||||
if c.Codec == nil || c.Codec.Name() == "" {
|
|
||||||
return errorf(CodeUnknown, "no codec configured")
|
|
||||||
}
|
|
||||||
if c.RequestCompressionName != "" && c.RequestCompressionName != compressionIdentity {
|
|
||||||
if _, ok := c.CompressionPools[c.RequestCompressionName]; !ok {
|
|
||||||
return errorf(CodeUnknown, "unknown compression %q", c.RequestCompressionName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientConfig) protobuf() Codec {
|
|
||||||
if c.Codec.Name() == codecNameProto {
|
|
||||||
return c.Codec
|
|
||||||
}
|
|
||||||
return &protoBinaryCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientConfig) newSpec(t StreamType) Spec {
|
|
||||||
return Spec{
|
|
||||||
StreamType: t,
|
|
||||||
Procedure: c.Procedure,
|
|
||||||
Schema: c.Schema,
|
|
||||||
IsClient: true,
|
|
||||||
IdempotencyLevel: c.IdempotencyLevel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRequestURL(rawURL string) (*url.URL, *Error) {
|
|
||||||
url, err := url.ParseRequestURI(rawURL)
|
|
||||||
if err == nil {
|
|
||||||
return url, nil
|
|
||||||
}
|
|
||||||
if !strings.Contains(rawURL, "://") {
|
|
||||||
// URL doesn't have a scheme, so the user is likely accustomed to
|
|
||||||
// grpc-go's APIs.
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"URL %q missing scheme: use http:// or https:// (unlike grpc-go)",
|
|
||||||
rawURL,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return nil, NewError(CodeUnavailable, err)
|
|
||||||
}
|
|
||||||
-441
@@ -1,441 +0,0 @@
|
|||||||
// Copyright 2021-2025 The Connect Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// errNoStreamInitialized signals that a no stream has been initialized when
|
|
||||||
// attempting to access stream-related methods.
|
|
||||||
errNoStreamInitialized = errors.New("no stream initialized")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClientStreamForClient is the client's view of a client streaming RPC.
|
|
||||||
//
|
|
||||||
// It's returned from [Client].CallClientStream, but doesn't currently have an
|
|
||||||
// exported constructor function.
|
|
||||||
//
|
|
||||||
// When using this stream, request headers should be set via the [ClientStreamForClient.RequestHeader] method.
|
|
||||||
type ClientStreamForClient[Req, Res any] struct {
|
|
||||||
conn StreamingClientConn
|
|
||||||
initializer maybeInitializer
|
|
||||||
// Error from client construction. If non-nil, return for all calls.
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec returns the specification for the RPC.
|
|
||||||
func (c *ClientStreamForClient[_, _]) Spec() Spec {
|
|
||||||
return c.conn.Spec()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peer describes the server for the RPC.
|
|
||||||
func (c *ClientStreamForClient[_, _]) Peer() Peer {
|
|
||||||
return c.conn.Peer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestHeader returns the request headers. Headers are sent to the server with the
|
|
||||||
// first call to Send.
|
|
||||||
//
|
|
||||||
// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the
|
|
||||||
// Connect and gRPC protocols. Applications shouldn't write them.
|
|
||||||
func (c *ClientStreamForClient[Req, Res]) RequestHeader() http.Header {
|
|
||||||
if c.err != nil {
|
|
||||||
return http.Header{}
|
|
||||||
}
|
|
||||||
return c.conn.RequestHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message to the server. The first call to Send also sends the request
|
|
||||||
// headers.
|
|
||||||
//
|
|
||||||
// If the server returns an error, Send returns an error that wraps [io.EOF].
|
|
||||||
// Clients should check for case using the standard library's [errors.Is] and
|
|
||||||
// unmarshal the error using CloseAndReceive.
|
|
||||||
func (c *ClientStreamForClient[Req, Res]) Send(request *Req) error {
|
|
||||||
if c.err != nil {
|
|
||||||
return c.err
|
|
||||||
}
|
|
||||||
if request == nil {
|
|
||||||
return c.conn.Send(nil)
|
|
||||||
}
|
|
||||||
return c.conn.Send(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseAndReceive closes the send side of the stream and waits for the
|
|
||||||
// response.
|
|
||||||
func (c *ClientStreamForClient[Req, Res]) CloseAndReceive() (*Response[Res], error) {
|
|
||||||
if c.err != nil {
|
|
||||||
return nil, c.err
|
|
||||||
}
|
|
||||||
if err := c.conn.CloseRequest(); err != nil {
|
|
||||||
_ = c.conn.CloseResponse()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response, err := receiveUnaryResponse[Res](c.conn, c.initializer)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.conn.CloseResponse()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return response, c.conn.CloseResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conn exposes the underlying StreamingClientConn. This may be useful if
|
|
||||||
// you'd prefer to wrap the connection in a different high-level API.
|
|
||||||
func (c *ClientStreamForClient[Req, Res]) Conn() (StreamingClientConn, error) {
|
|
||||||
return c.conn, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientStreamForClientSimple is the client's view of a client streaming RPC.
|
|
||||||
//
|
|
||||||
// It's returned from [Client.CallClientStreamSimple], but doesn't currently have an
|
|
||||||
// exported constructor function.
|
|
||||||
//
|
|
||||||
// Usage of this stream requires that request headers be set in a [CallInfo] object in context via [NewClientContext].
|
|
||||||
// In addition, the response returned by [ClientStreamForClientSimple.CloseAndReceive] is the response type defined for
|
|
||||||
// the stream and _not_ a Connect [Response] wrapper type. As a result, response headers/trailers should be read from
|
|
||||||
// the [CallInfo] object in context.
|
|
||||||
type ClientStreamForClientSimple[Req, Res any] struct {
|
|
||||||
stream *ClientStreamForClient[Req, Res]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec returns the specification for the RPC.
|
|
||||||
func (c *ClientStreamForClientSimple[_, _]) Spec() Spec {
|
|
||||||
if c.stream == nil {
|
|
||||||
return Spec{}
|
|
||||||
}
|
|
||||||
return c.stream.Spec()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peer describes the server for the RPC.
|
|
||||||
func (c *ClientStreamForClientSimple[_, _]) Peer() Peer {
|
|
||||||
if c.stream == nil {
|
|
||||||
return Peer{}
|
|
||||||
}
|
|
||||||
return c.stream.Peer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message to the server. The first call to Send also sends the request
|
|
||||||
// headers.
|
|
||||||
//
|
|
||||||
// If the server returns an error, Send returns an error that wraps [io.EOF].
|
|
||||||
// Clients should check for case using the standard library's [errors.Is] and
|
|
||||||
// unmarshal the error using CloseAndReceive.
|
|
||||||
func (c *ClientStreamForClientSimple[Req, Res]) Send(request *Req) error {
|
|
||||||
if c.stream == nil {
|
|
||||||
return errNoStreamInitialized
|
|
||||||
}
|
|
||||||
return c.stream.Send(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseAndReceive closes the send side of the stream and waits for the
|
|
||||||
// response.
|
|
||||||
func (c *ClientStreamForClientSimple[Req, Res]) CloseAndReceive() (*Res, error) {
|
|
||||||
if c.stream == nil {
|
|
||||||
return nil, errNoStreamInitialized
|
|
||||||
}
|
|
||||||
res, err := c.stream.CloseAndReceive()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return res.Msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerStreamForClient is the client's view of a server streaming RPC.
|
|
||||||
//
|
|
||||||
// It's returned from [Client].CallServerStream, but doesn't currently have an
|
|
||||||
// exported constructor function.
|
|
||||||
type ServerStreamForClient[Res any] struct {
|
|
||||||
conn StreamingClientConn
|
|
||||||
initializer maybeInitializer
|
|
||||||
msg *Res
|
|
||||||
// Error from client construction. If non-nil, return for all calls.
|
|
||||||
constructErr error
|
|
||||||
// Error from conn.Receive().
|
|
||||||
receiveErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive advances the stream to the next message, which will then be
|
|
||||||
// available through the Msg method. It returns false when the stream stops,
|
|
||||||
// either by reaching the end or by encountering an unexpected error. After
|
|
||||||
// Receive returns false, the Err method will return any unexpected error
|
|
||||||
// encountered.
|
|
||||||
func (s *ServerStreamForClient[Res]) Receive() bool {
|
|
||||||
if s.constructErr != nil || s.receiveErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
s.msg = new(Res)
|
|
||||||
if err := s.initializer.maybe(s.conn.Spec(), s.msg); err != nil {
|
|
||||||
s.receiveErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
s.receiveErr = s.conn.Receive(s.msg)
|
|
||||||
return s.receiveErr == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Msg returns the most recent message unmarshaled by a call to Receive.
|
|
||||||
func (s *ServerStreamForClient[Res]) Msg() *Res {
|
|
||||||
if s.msg == nil {
|
|
||||||
s.msg = new(Res)
|
|
||||||
}
|
|
||||||
return s.msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Err returns the first non-EOF error that was encountered by Receive.
|
|
||||||
func (s *ServerStreamForClient[Res]) Err() error {
|
|
||||||
if s.constructErr != nil {
|
|
||||||
return s.constructErr
|
|
||||||
}
|
|
||||||
if s.receiveErr != nil && !errors.Is(s.receiveErr, io.EOF) {
|
|
||||||
return s.receiveErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseHeader returns the headers received from the server. It blocks until
|
|
||||||
// the first call to Receive returns.
|
|
||||||
func (s *ServerStreamForClient[Res]) ResponseHeader() http.Header {
|
|
||||||
if s.constructErr != nil {
|
|
||||||
return http.Header{}
|
|
||||||
}
|
|
||||||
return s.conn.ResponseHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseTrailer returns the trailers received from the server. Trailers
|
|
||||||
// aren't fully populated until Receive() returns an error wrapping io.EOF.
|
|
||||||
func (s *ServerStreamForClient[Res]) ResponseTrailer() http.Header {
|
|
||||||
if s.constructErr != nil {
|
|
||||||
return http.Header{}
|
|
||||||
}
|
|
||||||
return s.conn.ResponseTrailer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the receive side of the stream.
|
|
||||||
//
|
|
||||||
// Close is non-blocking. To gracefully close the stream and allow for
|
|
||||||
// connection resuse ensure all messages have been received before calling
|
|
||||||
// Close. All messages are received when Receive returns false.
|
|
||||||
func (s *ServerStreamForClient[Res]) Close() error {
|
|
||||||
if s.constructErr != nil {
|
|
||||||
return s.constructErr
|
|
||||||
}
|
|
||||||
return s.conn.CloseResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conn exposes the underlying StreamingClientConn. This may be useful if
|
|
||||||
// you'd prefer to wrap the connection in a different high-level API.
|
|
||||||
func (s *ServerStreamForClient[Res]) Conn() (StreamingClientConn, error) {
|
|
||||||
return s.conn, s.constructErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// BidiStreamForClient is the client's view of a bidirectional streaming RPC.
|
|
||||||
//
|
|
||||||
// It's returned from [Client].CallBidiStream, but doesn't currently have an
|
|
||||||
// exported constructor function.
|
|
||||||
type BidiStreamForClient[Req, Res any] struct {
|
|
||||||
conn StreamingClientConn
|
|
||||||
initializer maybeInitializer
|
|
||||||
// Error from client construction. If non-nil, return for all calls.
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec returns the specification for the RPC.
|
|
||||||
func (b *BidiStreamForClient[_, _]) Spec() Spec {
|
|
||||||
return b.conn.Spec()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peer describes the server for the RPC.
|
|
||||||
func (b *BidiStreamForClient[_, _]) Peer() Peer {
|
|
||||||
return b.conn.Peer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestHeader returns the request headers. Headers are sent with the first
|
|
||||||
// call to Send.
|
|
||||||
//
|
|
||||||
// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the
|
|
||||||
// Connect and gRPC protocols. Applications shouldn't write them.
|
|
||||||
func (b *BidiStreamForClient[Req, Res]) RequestHeader() http.Header {
|
|
||||||
if b.err != nil {
|
|
||||||
return http.Header{}
|
|
||||||
}
|
|
||||||
return b.conn.RequestHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message to the server. The first call to Send also sends the request
|
|
||||||
// headers. To send just the request headers, without a body, call Send with a
|
|
||||||
// nil pointer.
|
|
||||||
//
|
|
||||||
// If the server returns an error, Send returns an error that wraps [io.EOF].
|
|
||||||
// Clients should check for EOF using the standard library's [errors.Is] and
|
|
||||||
// call Receive to retrieve the error.
|
|
||||||
func (b *BidiStreamForClient[Req, Res]) Send(msg *Req) error {
|
|
||||||
if b.err != nil {
|
|
||||||
return b.err
|
|
||||||
}
|
|
||||||
if msg == nil {
|
|
||||||
return b.conn.Send(nil)
|
|
||||||
}
|
|
||||||
return b.conn.Send(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseRequest closes the send side of the stream.
|
|
||||||
func (b *BidiStreamForClient[Req, Res]) CloseRequest() error {
|
|
||||||
if b.err != nil {
|
|
||||||
return b.err
|
|
||||||
}
|
|
||||||
return b.conn.CloseRequest()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive a message. When the server is done sending messages and no other
|
|
||||||
// errors have occurred, Receive will return an error that wraps [io.EOF].
|
|
||||||
func (b *BidiStreamForClient[Req, Res]) Receive() (*Res, error) {
|
|
||||||
if b.err != nil {
|
|
||||||
return nil, b.err
|
|
||||||
}
|
|
||||||
var msg Res
|
|
||||||
if err := b.initializer.maybe(b.conn.Spec(), &msg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := b.conn.Receive(&msg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseResponse closes the receive side of the stream.
|
|
||||||
//
|
|
||||||
// CloseResponse is non-blocking. To gracefully close the stream and allow for
|
|
||||||
// connection resuse ensure all messages have been received before calling
|
|
||||||
// CloseResponse. All messages are received when Receive returns an error
|
|
||||||
// wrapping [io.EOF].
|
|
||||||
func (b *BidiStreamForClient[Req, Res]) CloseResponse() error {
|
|
||||||
if b.err != nil {
|
|
||||||
return b.err
|
|
||||||
}
|
|
||||||
return b.conn.CloseResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseHeader returns the headers received from the server. It blocks until
|
|
||||||
// the first call to Receive returns.
|
|
||||||
func (b *BidiStreamForClient[Req, Res]) ResponseHeader() http.Header {
|
|
||||||
if b.err != nil {
|
|
||||||
return http.Header{}
|
|
||||||
}
|
|
||||||
return b.conn.ResponseHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseTrailer returns the trailers received from the server. Trailers
|
|
||||||
// aren't fully populated until Receive() returns an error wrapping [io.EOF].
|
|
||||||
func (b *BidiStreamForClient[Req, Res]) ResponseTrailer() http.Header {
|
|
||||||
if b.err != nil {
|
|
||||||
return http.Header{}
|
|
||||||
}
|
|
||||||
return b.conn.ResponseTrailer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conn exposes the underlying StreamingClientConn. This may be useful if
|
|
||||||
// you'd prefer to wrap the connection in a different high-level API.
|
|
||||||
func (b *BidiStreamForClient[Req, Res]) Conn() (StreamingClientConn, error) {
|
|
||||||
return b.conn, b.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BidiStreamForClientSimple is the client's view of a bidirectional streaming RPC.
|
|
||||||
//
|
|
||||||
// It's returned from [Client].CallBidiStream, but doesn't currently have an
|
|
||||||
// exported constructor function.
|
|
||||||
type BidiStreamForClientSimple[Req, Res any] struct {
|
|
||||||
stream *BidiStreamForClient[Req, Res]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec returns the specification for the RPC.
|
|
||||||
func (b *BidiStreamForClientSimple[_, _]) Spec() Spec {
|
|
||||||
if b.stream == nil {
|
|
||||||
return Spec{}
|
|
||||||
}
|
|
||||||
return b.stream.Spec()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peer describes the server for the RPC.
|
|
||||||
func (b *BidiStreamForClientSimple[_, _]) Peer() Peer {
|
|
||||||
if b.stream == nil {
|
|
||||||
return Peer{}
|
|
||||||
}
|
|
||||||
return b.stream.Peer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message to the server. The first call to Send also sends the request
|
|
||||||
// headers. To send just the request headers, without a body, call Send with a
|
|
||||||
// nil pointer.
|
|
||||||
//
|
|
||||||
// If the server returns an error, Send returns an error that wraps [io.EOF].
|
|
||||||
// Clients should check for EOF using the standard library's [errors.Is] and
|
|
||||||
// call Receive to retrieve the error.
|
|
||||||
func (b *BidiStreamForClientSimple[Req, Res]) Send(msg *Req) error {
|
|
||||||
if b.stream == nil {
|
|
||||||
return errNoStreamInitialized
|
|
||||||
}
|
|
||||||
return b.stream.Send(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseRequest closes the send side of the stream.
|
|
||||||
func (b *BidiStreamForClientSimple[Req, Res]) CloseRequest() error {
|
|
||||||
if b.stream == nil {
|
|
||||||
return errNoStreamInitialized
|
|
||||||
}
|
|
||||||
return b.stream.CloseRequest()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive a message. When the server is done sending messages and no other
|
|
||||||
// errors have occurred, Receive will return an error that wraps [io.EOF].
|
|
||||||
func (b *BidiStreamForClientSimple[Req, Res]) Receive() (*Res, error) {
|
|
||||||
if b.stream == nil {
|
|
||||||
return nil, errNoStreamInitialized
|
|
||||||
}
|
|
||||||
return b.stream.Receive()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseResponse closes the receive side of the stream.
|
|
||||||
//
|
|
||||||
// CloseResponse is non-blocking. To gracefully close the stream and allow for
|
|
||||||
// connection resuse ensure all messages have been received before calling
|
|
||||||
// CloseResponse. All messages are received when Receive returns an error
|
|
||||||
// wrapping [io.EOF].
|
|
||||||
func (b *BidiStreamForClientSimple[Req, Res]) CloseResponse() error {
|
|
||||||
if b.stream == nil {
|
|
||||||
return errNoStreamInitialized
|
|
||||||
}
|
|
||||||
return b.stream.CloseResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseHeader returns the headers received from the server. It blocks until
|
|
||||||
// the first call to Receive returns.
|
|
||||||
func (b *BidiStreamForClientSimple[Req, Res]) ResponseHeader() http.Header {
|
|
||||||
if b.stream == nil {
|
|
||||||
return make(http.Header)
|
|
||||||
}
|
|
||||||
return b.stream.ResponseHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseTrailer returns the trailers received from the server. Trailers
|
|
||||||
// aren't fully populated until Receive() returns an error wrapping [io.EOF].
|
|
||||||
func (b *BidiStreamForClientSimple[Req, Res]) ResponseTrailer() http.Header {
|
|
||||||
if b.stream == nil {
|
|
||||||
return make(http.Header)
|
|
||||||
}
|
|
||||||
return b.stream.ResponseTrailer()
|
|
||||||
}
|
|
||||||
-226
@@ -1,226 +0,0 @@
|
|||||||
// Copyright 2021-2025 The Connect Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Code is one of the Connect protocol's error codes. There are no user-defined
|
|
||||||
// codes, so only the codes enumerated below are valid. In both name and
|
|
||||||
// semantics, these codes match the gRPC status codes.
|
|
||||||
//
|
|
||||||
// The descriptions below are optimized for brevity rather than completeness.
|
|
||||||
// See the [Connect protocol specification] for detailed descriptions of each
|
|
||||||
// code and example usage.
|
|
||||||
//
|
|
||||||
// [Connect protocol specification]: https://connectrpc.com/docs/protocol
|
|
||||||
type Code uint32
|
|
||||||
|
|
||||||
const (
|
|
||||||
// The zero code in gRPC is OK, which indicates that the operation was a
|
|
||||||
// success. We don't define a constant for it because it overlaps awkwardly
|
|
||||||
// with Go's error semantics: what does it mean to have a non-nil error with
|
|
||||||
// an OK status? (Also, the Connect protocol doesn't use a code for
|
|
||||||
// successes.)
|
|
||||||
|
|
||||||
// CodeCanceled indicates that the operation was canceled, typically by the
|
|
||||||
// caller.
|
|
||||||
CodeCanceled Code = 1
|
|
||||||
|
|
||||||
// CodeUnknown indicates that the operation failed for an unknown reason.
|
|
||||||
CodeUnknown Code = 2
|
|
||||||
|
|
||||||
// CodeInvalidArgument indicates that client supplied an invalid argument.
|
|
||||||
CodeInvalidArgument Code = 3
|
|
||||||
|
|
||||||
// CodeDeadlineExceeded indicates that deadline expired before the operation
|
|
||||||
// could complete.
|
|
||||||
CodeDeadlineExceeded Code = 4
|
|
||||||
|
|
||||||
// CodeNotFound indicates that some requested entity (for example, a file or
|
|
||||||
// directory) was not found.
|
|
||||||
CodeNotFound Code = 5
|
|
||||||
|
|
||||||
// CodeAlreadyExists indicates that client attempted to create an entity (for
|
|
||||||
// example, a file or directory) that already exists.
|
|
||||||
CodeAlreadyExists Code = 6
|
|
||||||
|
|
||||||
// CodePermissionDenied indicates that the caller doesn't have permission to
|
|
||||||
// execute the specified operation.
|
|
||||||
CodePermissionDenied Code = 7
|
|
||||||
|
|
||||||
// CodeResourceExhausted indicates that some resource has been exhausted. For
|
|
||||||
// example, a per-user quota may be exhausted or the entire file system may
|
|
||||||
// be full.
|
|
||||||
CodeResourceExhausted Code = 8
|
|
||||||
|
|
||||||
// CodeFailedPrecondition indicates that the system is not in a state
|
|
||||||
// required for the operation's execution.
|
|
||||||
CodeFailedPrecondition Code = 9
|
|
||||||
|
|
||||||
// CodeAborted indicates that operation was aborted by the system, usually
|
|
||||||
// because of a concurrency issue such as a sequencer check failure or
|
|
||||||
// transaction abort.
|
|
||||||
CodeAborted Code = 10
|
|
||||||
|
|
||||||
// CodeOutOfRange indicates that the operation was attempted past the valid
|
|
||||||
// range (for example, seeking past end-of-file).
|
|
||||||
CodeOutOfRange Code = 11
|
|
||||||
|
|
||||||
// CodeUnimplemented indicates that the operation isn't implemented,
|
|
||||||
// supported, or enabled in this service.
|
|
||||||
CodeUnimplemented Code = 12
|
|
||||||
|
|
||||||
// CodeInternal indicates that some invariants expected by the underlying
|
|
||||||
// system have been broken. This code is reserved for serious errors.
|
|
||||||
CodeInternal Code = 13
|
|
||||||
|
|
||||||
// CodeUnavailable indicates that the service is currently unavailable. This
|
|
||||||
// is usually temporary, so clients can back off and retry idempotent
|
|
||||||
// operations.
|
|
||||||
CodeUnavailable Code = 14
|
|
||||||
|
|
||||||
// CodeDataLoss indicates that the operation has resulted in unrecoverable
|
|
||||||
// data loss or corruption.
|
|
||||||
CodeDataLoss Code = 15
|
|
||||||
|
|
||||||
// CodeUnauthenticated indicates that the request does not have valid
|
|
||||||
// authentication credentials for the operation.
|
|
||||||
CodeUnauthenticated Code = 16
|
|
||||||
|
|
||||||
minCode = CodeCanceled
|
|
||||||
maxCode = CodeUnauthenticated
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c Code) String() string {
|
|
||||||
switch c {
|
|
||||||
case CodeCanceled:
|
|
||||||
return "canceled"
|
|
||||||
case CodeUnknown:
|
|
||||||
return "unknown"
|
|
||||||
case CodeInvalidArgument:
|
|
||||||
return "invalid_argument"
|
|
||||||
case CodeDeadlineExceeded:
|
|
||||||
return "deadline_exceeded"
|
|
||||||
case CodeNotFound:
|
|
||||||
return "not_found"
|
|
||||||
case CodeAlreadyExists:
|
|
||||||
return "already_exists"
|
|
||||||
case CodePermissionDenied:
|
|
||||||
return "permission_denied"
|
|
||||||
case CodeResourceExhausted:
|
|
||||||
return "resource_exhausted"
|
|
||||||
case CodeFailedPrecondition:
|
|
||||||
return "failed_precondition"
|
|
||||||
case CodeAborted:
|
|
||||||
return "aborted"
|
|
||||||
case CodeOutOfRange:
|
|
||||||
return "out_of_range"
|
|
||||||
case CodeUnimplemented:
|
|
||||||
return "unimplemented"
|
|
||||||
case CodeInternal:
|
|
||||||
return "internal"
|
|
||||||
case CodeUnavailable:
|
|
||||||
return "unavailable"
|
|
||||||
case CodeDataLoss:
|
|
||||||
return "data_loss"
|
|
||||||
case CodeUnauthenticated:
|
|
||||||
return "unauthenticated"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("code_%d", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText implements [encoding.TextMarshaler].
|
|
||||||
func (c Code) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(c.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText implements [encoding.TextUnmarshaler].
|
|
||||||
func (c *Code) UnmarshalText(data []byte) error {
|
|
||||||
dataStr := string(data)
|
|
||||||
switch dataStr {
|
|
||||||
case "canceled":
|
|
||||||
*c = CodeCanceled
|
|
||||||
return nil
|
|
||||||
case "unknown":
|
|
||||||
*c = CodeUnknown
|
|
||||||
return nil
|
|
||||||
case "invalid_argument":
|
|
||||||
*c = CodeInvalidArgument
|
|
||||||
return nil
|
|
||||||
case "deadline_exceeded":
|
|
||||||
*c = CodeDeadlineExceeded
|
|
||||||
return nil
|
|
||||||
case "not_found":
|
|
||||||
*c = CodeNotFound
|
|
||||||
return nil
|
|
||||||
case "already_exists":
|
|
||||||
*c = CodeAlreadyExists
|
|
||||||
return nil
|
|
||||||
case "permission_denied":
|
|
||||||
*c = CodePermissionDenied
|
|
||||||
return nil
|
|
||||||
case "resource_exhausted":
|
|
||||||
*c = CodeResourceExhausted
|
|
||||||
return nil
|
|
||||||
case "failed_precondition":
|
|
||||||
*c = CodeFailedPrecondition
|
|
||||||
return nil
|
|
||||||
case "aborted":
|
|
||||||
*c = CodeAborted
|
|
||||||
return nil
|
|
||||||
case "out_of_range":
|
|
||||||
*c = CodeOutOfRange
|
|
||||||
return nil
|
|
||||||
case "unimplemented":
|
|
||||||
*c = CodeUnimplemented
|
|
||||||
return nil
|
|
||||||
case "internal":
|
|
||||||
*c = CodeInternal
|
|
||||||
return nil
|
|
||||||
case "unavailable":
|
|
||||||
*c = CodeUnavailable
|
|
||||||
return nil
|
|
||||||
case "data_loss":
|
|
||||||
*c = CodeDataLoss
|
|
||||||
return nil
|
|
||||||
case "unauthenticated":
|
|
||||||
*c = CodeUnauthenticated
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Ensure that non-canonical codes round-trip through MarshalText and
|
|
||||||
// UnmarshalText.
|
|
||||||
if after, ok := strings.CutPrefix(dataStr, "code_"); ok {
|
|
||||||
dataStr = after
|
|
||||||
code, err := strconv.ParseUint(dataStr, 10 /* base */, 32 /* bitsize */)
|
|
||||||
if err == nil && (code < uint64(minCode) || code > uint64(maxCode)) {
|
|
||||||
*c = Code(code)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("invalid code %q", dataStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CodeOf returns the error's status code if it is or wraps an [*Error] and
|
|
||||||
// [CodeUnknown] otherwise.
|
|
||||||
func CodeOf(err error) Code {
|
|
||||||
if connectErr, ok := asError(err); ok {
|
|
||||||
return connectErr.Code()
|
|
||||||
}
|
|
||||||
return CodeUnknown
|
|
||||||
}
|
|
||||||
-259
@@ -1,259 +0,0 @@
|
|||||||
// Copyright 2021-2025 The Connect Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/runtime/protoiface"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
codecNameProto = "proto"
|
|
||||||
codecNameJSON = "json"
|
|
||||||
codecNameJSONCharsetUTF8 = codecNameJSON + "; charset=utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Codec marshals structs (typically generated from a schema) to and from bytes.
|
|
||||||
type Codec interface {
|
|
||||||
// Name returns the name of the Codec.
|
|
||||||
//
|
|
||||||
// This may be used as part of the Content-Type within HTTP. For example,
|
|
||||||
// with gRPC this is the content subtype, so "application/grpc+proto" will
|
|
||||||
// map to the Codec with name "proto".
|
|
||||||
//
|
|
||||||
// Names must not be empty.
|
|
||||||
Name() string
|
|
||||||
// Marshal marshals the given message.
|
|
||||||
//
|
|
||||||
// Marshal may expect a specific type of message, and will error if this type
|
|
||||||
// is not given.
|
|
||||||
Marshal(any) ([]byte, error)
|
|
||||||
// Unmarshal unmarshals the given message.
|
|
||||||
//
|
|
||||||
// Unmarshal may expect a specific type of message, and will error if this
|
|
||||||
// type is not given.
|
|
||||||
Unmarshal([]byte, any) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// marshalAppender is an extension to Codec for appending to a byte slice.
|
|
||||||
type marshalAppender interface {
|
|
||||||
Codec
|
|
||||||
|
|
||||||
// MarshalAppend marshals the given message and appends it to the given
|
|
||||||
// byte slice.
|
|
||||||
//
|
|
||||||
// MarshalAppend may expect a specific type of message, and will error if
|
|
||||||
// this type is not given.
|
|
||||||
MarshalAppend([]byte, any) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stableCodec is an extension to Codec for serializing with stable output.
|
|
||||||
type stableCodec interface {
|
|
||||||
Codec
|
|
||||||
|
|
||||||
// MarshalStable marshals the given message with stable field ordering.
|
|
||||||
//
|
|
||||||
// MarshalStable should return the same output for a given input. Although
|
|
||||||
// it is not guaranteed to be canonicalized, the marshalling routine for
|
|
||||||
// MarshalStable will opt for the most normalized output available for a
|
|
||||||
// given serialization.
|
|
||||||
//
|
|
||||||
// For practical reasons, it is possible for MarshalStable to return two
|
|
||||||
// different results for two inputs considered to be "equal" in their own
|
|
||||||
// domain, and it may change in the future with codec updates, but for
|
|
||||||
// any given concrete value and any given version, it should return the
|
|
||||||
// same output.
|
|
||||||
MarshalStable(any) ([]byte, error)
|
|
||||||
|
|
||||||
// IsBinary returns true if the marshalled data is binary for this codec.
|
|
||||||
//
|
|
||||||
// If this function returns false, the data returned from Marshal and
|
|
||||||
// MarshalStable are considered valid text and may be used in contexts
|
|
||||||
// where text is expected.
|
|
||||||
IsBinary() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type protoBinaryCodec struct{}
|
|
||||||
|
|
||||||
var _ Codec = (*protoBinaryCodec)(nil)
|
|
||||||
|
|
||||||
func (c *protoBinaryCodec) Name() string { return codecNameProto }
|
|
||||||
|
|
||||||
func (c *protoBinaryCodec) Marshal(message any) ([]byte, error) {
|
|
||||||
protoMessage, ok := message.(proto.Message)
|
|
||||||
if !ok {
|
|
||||||
return nil, errNotProto(message)
|
|
||||||
}
|
|
||||||
return proto.Marshal(protoMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protoBinaryCodec) MarshalAppend(dst []byte, message any) ([]byte, error) {
|
|
||||||
protoMessage, ok := message.(proto.Message)
|
|
||||||
if !ok {
|
|
||||||
return nil, errNotProto(message)
|
|
||||||
}
|
|
||||||
return proto.MarshalOptions{}.MarshalAppend(dst, protoMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protoBinaryCodec) Unmarshal(data []byte, message any) error {
|
|
||||||
protoMessage, ok := message.(proto.Message)
|
|
||||||
if !ok {
|
|
||||||
return errNotProto(message)
|
|
||||||
}
|
|
||||||
err := proto.Unmarshal(data, protoMessage)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unmarshal into %T: %w", message, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protoBinaryCodec) MarshalStable(message any) ([]byte, error) {
|
|
||||||
protoMessage, ok := message.(proto.Message)
|
|
||||||
if !ok {
|
|
||||||
return nil, errNotProto(message)
|
|
||||||
}
|
|
||||||
// protobuf does not offer a canonical output today, so this format is not
|
|
||||||
// guaranteed to match deterministic output from other protobuf libraries.
|
|
||||||
// In addition, unknown fields may cause inconsistent output for otherwise
|
|
||||||
// equal messages.
|
|
||||||
// https://github.com/golang/protobuf/issues/1121
|
|
||||||
options := proto.MarshalOptions{Deterministic: true}
|
|
||||||
return options.Marshal(protoMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protoBinaryCodec) IsBinary() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type protoJSONCodec struct {
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Codec = (*protoJSONCodec)(nil)
|
|
||||||
|
|
||||||
func (c *protoJSONCodec) Name() string { return c.name }
|
|
||||||
|
|
||||||
func (c *protoJSONCodec) Marshal(message any) ([]byte, error) {
|
|
||||||
protoMessage, ok := message.(proto.Message)
|
|
||||||
if !ok {
|
|
||||||
return nil, errNotProto(message)
|
|
||||||
}
|
|
||||||
return protojson.MarshalOptions{}.Marshal(protoMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protoJSONCodec) MarshalAppend(dst []byte, message any) ([]byte, error) {
|
|
||||||
protoMessage, ok := message.(proto.Message)
|
|
||||||
if !ok {
|
|
||||||
return nil, errNotProto(message)
|
|
||||||
}
|
|
||||||
return protojson.MarshalOptions{}.MarshalAppend(dst, protoMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protoJSONCodec) Unmarshal(binary []byte, message any) error {
|
|
||||||
protoMessage, ok := message.(proto.Message)
|
|
||||||
if !ok {
|
|
||||||
return errNotProto(message)
|
|
||||||
}
|
|
||||||
if len(binary) == 0 {
|
|
||||||
return errors.New("zero-length payload is not a valid JSON object")
|
|
||||||
}
|
|
||||||
// Discard unknown fields so clients and servers aren't forced to always use
|
|
||||||
// exactly the same version of the schema.
|
|
||||||
options := protojson.UnmarshalOptions{DiscardUnknown: true}
|
|
||||||
err := options.Unmarshal(binary, protoMessage)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unmarshal into %T: %w", message, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protoJSONCodec) MarshalStable(message any) ([]byte, error) {
|
|
||||||
// protojson does not offer a "deterministic" field ordering, but fields
|
|
||||||
// are still ordered consistently by their index. However, protojson can
|
|
||||||
// output inconsistent whitespace for some reason, therefore it is
|
|
||||||
// suggested to use a formatter to ensure consistent formatting.
|
|
||||||
// https://github.com/golang/protobuf/issues/1373
|
|
||||||
messageJSON, err := c.Marshal(message)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
compactedJSON := bytes.NewBuffer(messageJSON[:0])
|
|
||||||
if err = json.Compact(compactedJSON, messageJSON); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return compactedJSON.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protoJSONCodec) IsBinary() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// readOnlyCodecs is a read-only interface to a map of named codecs.
|
|
||||||
type readOnlyCodecs interface {
|
|
||||||
// Get gets the Codec with the given name.
|
|
||||||
Get(string) Codec
|
|
||||||
// Protobuf gets the user-supplied protobuf codec, falling back to the default
|
|
||||||
// implementation if necessary.
|
|
||||||
//
|
|
||||||
// This is helpful in the gRPC protocol, where the wire protocol requires
|
|
||||||
// marshaling protobuf structs to binary even if the RPC procedures were
|
|
||||||
// generated from a different IDL.
|
|
||||||
Protobuf() Codec
|
|
||||||
// Names returns a copy of the registered codec names. The returned slice is
|
|
||||||
// safe for the caller to mutate.
|
|
||||||
Names() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newReadOnlyCodecs(nameToCodec map[string]Codec) readOnlyCodecs {
|
|
||||||
return &codecMap{
|
|
||||||
nameToCodec: nameToCodec,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type codecMap struct {
|
|
||||||
nameToCodec map[string]Codec
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *codecMap) Get(name string) Codec {
|
|
||||||
return m.nameToCodec[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *codecMap) Protobuf() Codec {
|
|
||||||
if pb, ok := m.nameToCodec[codecNameProto]; ok {
|
|
||||||
return pb
|
|
||||||
}
|
|
||||||
return &protoBinaryCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *codecMap) Names() []string {
|
|
||||||
names := make([]string, 0, len(m.nameToCodec))
|
|
||||||
for name := range m.nameToCodec {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
func errNotProto(message any) error {
|
|
||||||
if _, ok := message.(protoiface.MessageV1); ok {
|
|
||||||
return fmt.Errorf("%T uses github.com/golang/protobuf, but connect-go only supports google.golang.org/protobuf: see https://go.dev/blog/protobuf-apiv2", message)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%T doesn't implement proto.Message", message)
|
|
||||||
}
|
|
||||||
-224
@@ -1,224 +0,0 @@
|
|||||||
// Copyright 2021-2025 The Connect Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
compressionGzip = "gzip"
|
|
||||||
compressionIdentity = "identity"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Decompressor is a reusable wrapper that decompresses an underlying data
|
|
||||||
// source. The standard library's [*gzip.Reader] implements Decompressor.
|
|
||||||
type Decompressor interface {
|
|
||||||
io.Reader
|
|
||||||
|
|
||||||
// Close closes the Decompressor, but not the underlying data source. It may
|
|
||||||
// return an error if the Decompressor wasn't read to EOF.
|
|
||||||
Close() error
|
|
||||||
|
|
||||||
// Reset discards the Decompressor's internal state, if any, and prepares it
|
|
||||||
// to read from a new source of compressed data.
|
|
||||||
Reset(io.Reader) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Compressor is a reusable wrapper that compresses data written to an
|
|
||||||
// underlying sink. The standard library's [*gzip.Writer] implements Compressor.
|
|
||||||
type Compressor interface {
|
|
||||||
io.Writer
|
|
||||||
|
|
||||||
// Close flushes any buffered data to the underlying sink, then closes the
|
|
||||||
// Compressor. It must not close the underlying sink.
|
|
||||||
Close() error
|
|
||||||
|
|
||||||
// Reset discards the Compressor's internal state, if any, and prepares it to
|
|
||||||
// write compressed data to a new sink.
|
|
||||||
Reset(io.Writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
type compressionPool struct {
|
|
||||||
decompressors sync.Pool
|
|
||||||
compressors sync.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCompressionPool(
|
|
||||||
newDecompressor func() Decompressor,
|
|
||||||
newCompressor func() Compressor,
|
|
||||||
) *compressionPool {
|
|
||||||
if newDecompressor == nil && newCompressor == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &compressionPool{
|
|
||||||
decompressors: sync.Pool{
|
|
||||||
New: func() any { return newDecompressor() },
|
|
||||||
},
|
|
||||||
compressors: sync.Pool{
|
|
||||||
New: func() any { return newCompressor() },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *compressionPool) Decompress(dst *bytes.Buffer, src *bytes.Buffer, readMaxBytes int64) *Error {
|
|
||||||
decompressor, err := c.getDecompressor(src)
|
|
||||||
if err != nil {
|
|
||||||
return errorf(CodeInvalidArgument, "get decompressor: %w", err)
|
|
||||||
}
|
|
||||||
reader := io.Reader(decompressor)
|
|
||||||
if readMaxBytes > 0 && readMaxBytes < math.MaxInt64 {
|
|
||||||
reader = io.LimitReader(decompressor, readMaxBytes+1)
|
|
||||||
}
|
|
||||||
bytesRead, err := dst.ReadFrom(reader)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.putDecompressor(decompressor)
|
|
||||||
err = wrapIfContextError(err)
|
|
||||||
if connectErr, ok := asError(err); ok {
|
|
||||||
return connectErr
|
|
||||||
}
|
|
||||||
return errorf(CodeInvalidArgument, "decompress: %w", err)
|
|
||||||
}
|
|
||||||
if readMaxBytes > 0 && bytesRead > readMaxBytes {
|
|
||||||
discardedBytes, err := io.Copy(io.Discard, decompressor)
|
|
||||||
_ = c.putDecompressor(decompressor)
|
|
||||||
if err != nil {
|
|
||||||
return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", readMaxBytes, err)
|
|
||||||
}
|
|
||||||
return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", bytesRead+discardedBytes, readMaxBytes)
|
|
||||||
}
|
|
||||||
if err := c.putDecompressor(decompressor); err != nil {
|
|
||||||
return errorf(CodeUnknown, "recycle decompressor: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *compressionPool) Compress(dst *bytes.Buffer, src *bytes.Buffer) *Error {
|
|
||||||
compressor, err := c.getCompressor(dst)
|
|
||||||
if err != nil {
|
|
||||||
return errorf(CodeUnknown, "get compressor: %w", err)
|
|
||||||
}
|
|
||||||
if _, err := src.WriteTo(compressor); err != nil {
|
|
||||||
_ = c.putCompressor(compressor)
|
|
||||||
err = wrapIfContextError(err)
|
|
||||||
if connectErr, ok := asError(err); ok {
|
|
||||||
return connectErr
|
|
||||||
}
|
|
||||||
return errorf(CodeInternal, "compress: %w", err)
|
|
||||||
}
|
|
||||||
if err := c.putCompressor(compressor); err != nil {
|
|
||||||
return errorf(CodeInternal, "recycle compressor: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *compressionPool) getDecompressor(reader io.Reader) (Decompressor, error) {
|
|
||||||
decompressor, ok := c.decompressors.Get().(Decompressor)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("expected Decompressor, got incorrect type from pool")
|
|
||||||
}
|
|
||||||
return decompressor, decompressor.Reset(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *compressionPool) putDecompressor(decompressor Decompressor) error {
|
|
||||||
if err := decompressor.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// While it's in the pool, we don't want the decompressor to retain a
|
|
||||||
// reference to the underlying reader. However, most decompressors attempt to
|
|
||||||
// read some header data from the new data source when Reset; since we don't
|
|
||||||
// know the compression format, we can't provide a valid header. Since we
|
|
||||||
// also reset the decompressor when it's pulled out of the pool, we can
|
|
||||||
// ignore errors here.
|
|
||||||
_ = decompressor.Reset(http.NoBody)
|
|
||||||
c.decompressors.Put(decompressor)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *compressionPool) getCompressor(writer io.Writer) (Compressor, error) {
|
|
||||||
compressor, ok := c.compressors.Get().(Compressor)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("expected Compressor, got incorrect type from pool")
|
|
||||||
}
|
|
||||||
compressor.Reset(writer)
|
|
||||||
return compressor, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *compressionPool) putCompressor(compressor Compressor) error {
|
|
||||||
if err := compressor.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
compressor.Reset(io.Discard) // don't keep references
|
|
||||||
c.compressors.Put(compressor)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readOnlyCompressionPools is a read-only interface to a map of named
|
|
||||||
// compressionPools.
|
|
||||||
type readOnlyCompressionPools interface {
|
|
||||||
Get(string) *compressionPool
|
|
||||||
Contains(string) bool
|
|
||||||
// Wordy, but clarifies how this is different from readOnlyCodecs.Names().
|
|
||||||
CommaSeparatedNames() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newReadOnlyCompressionPools(
|
|
||||||
nameToPool map[string]*compressionPool,
|
|
||||||
reversedNames []string,
|
|
||||||
) readOnlyCompressionPools {
|
|
||||||
// Client and handler configs keep compression names in registration order,
|
|
||||||
// but we want the last registered to be the most preferred.
|
|
||||||
names := make([]string, 0, len(reversedNames))
|
|
||||||
seen := make(map[string]struct{}, len(reversedNames))
|
|
||||||
for i := len(reversedNames) - 1; i >= 0; i-- {
|
|
||||||
name := reversedNames[i]
|
|
||||||
if _, ok := seen[name]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[name] = struct{}{}
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
return &namedCompressionPools{
|
|
||||||
nameToPool: nameToPool,
|
|
||||||
commaSeparatedNames: strings.Join(names, ","),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type namedCompressionPools struct {
|
|
||||||
nameToPool map[string]*compressionPool
|
|
||||||
commaSeparatedNames string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *namedCompressionPools) Get(name string) *compressionPool {
|
|
||||||
if name == "" || name == compressionIdentity {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return m.nameToPool[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *namedCompressionPools) Contains(name string) bool {
|
|
||||||
_, ok := m.nameToPool[name]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *namedCompressionPools) CommaSeparatedNames() string {
|
|
||||||
return m.commaSeparatedNames
|
|
||||||
}
|
|
||||||
-482
@@ -1,482 +0,0 @@
|
|||||||
// Copyright 2021-2025 The Connect Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
// Package connect is a slim RPC framework built on Protocol Buffers and
|
|
||||||
// [net/http]. In addition to supporting its own protocol, Connect handlers and
|
|
||||||
// clients are wire-compatible with gRPC and gRPC-Web, including streaming.
|
|
||||||
//
|
|
||||||
// This documentation is intended to explain each type and function in
|
|
||||||
// isolation. Walkthroughs, FAQs, and other narrative docs are available on the
|
|
||||||
// [Connect website], and there's a working [demonstration service] on Github.
|
|
||||||
//
|
|
||||||
// [Connect website]: https://connectrpc.com
|
|
||||||
// [demonstration service]: https://github.com/connectrpc/examples-go
|
|
||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Version is the semantic version of the connect module.
|
|
||||||
const Version = "1.19.1"
|
|
||||||
|
|
||||||
// These constants are used in compile-time handshakes with connect's generated
|
|
||||||
// code.
|
|
||||||
const (
|
|
||||||
IsAtLeastVersion0_0_1 = true
|
|
||||||
IsAtLeastVersion0_1_0 = true
|
|
||||||
IsAtLeastVersion1_7_0 = true
|
|
||||||
IsAtLeastVersion1_13_0 = true
|
|
||||||
)
|
|
||||||
|
|
||||||
// StreamType describes whether the client, server, neither, or both is
|
|
||||||
// streaming.
|
|
||||||
type StreamType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
StreamTypeUnary StreamType = 0b00
|
|
||||||
StreamTypeClient StreamType = 0b01
|
|
||||||
StreamTypeServer StreamType = 0b10
|
|
||||||
StreamTypeBidi = StreamTypeClient | StreamTypeServer
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s StreamType) String() string {
|
|
||||||
switch s {
|
|
||||||
case StreamTypeUnary:
|
|
||||||
return "unary"
|
|
||||||
case StreamTypeClient:
|
|
||||||
return "client"
|
|
||||||
case StreamTypeServer:
|
|
||||||
return "server"
|
|
||||||
case StreamTypeBidi:
|
|
||||||
return "bidi"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("stream_%d", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamingHandlerConn is the server's view of a bidirectional message
|
|
||||||
// exchange. Interceptors for streaming RPCs may wrap StreamingHandlerConns.
|
|
||||||
//
|
|
||||||
// Like the standard library's [http.ResponseWriter], StreamingHandlerConns write
|
|
||||||
// response headers to the network with the first call to Send. Any subsequent
|
|
||||||
// mutations are effectively no-ops. Handlers may mutate response trailers at
|
|
||||||
// any time before returning. When the client has finished sending data,
|
|
||||||
// Receive returns an error wrapping [io.EOF]. Handlers should check for this
|
|
||||||
// using the standard library's [errors.Is].
|
|
||||||
//
|
|
||||||
// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for
|
|
||||||
// use by the gRPC and Connect protocols: applications may read them but
|
|
||||||
// shouldn't write them.
|
|
||||||
//
|
|
||||||
// StreamingHandlerConn implementations provided by this module guarantee that
|
|
||||||
// all returned errors can be cast to [*Error] using the standard library's
|
|
||||||
// [errors.As].
|
|
||||||
//
|
|
||||||
// StreamingHandlerConn implementations do not need to be safe for concurrent use.
|
|
||||||
type StreamingHandlerConn interface {
|
|
||||||
Spec() Spec
|
|
||||||
Peer() Peer
|
|
||||||
|
|
||||||
Receive(any) error
|
|
||||||
RequestHeader() http.Header
|
|
||||||
|
|
||||||
Send(any) error
|
|
||||||
ResponseHeader() http.Header
|
|
||||||
ResponseTrailer() http.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamingClientConn is the client's view of a bidirectional message exchange.
|
|
||||||
// Interceptors for streaming RPCs may wrap StreamingClientConns.
|
|
||||||
//
|
|
||||||
// StreamingClientConns write request headers to the network with the first
|
|
||||||
// call to Send. Any subsequent mutations are effectively no-ops. When the
|
|
||||||
// server is done sending data, the StreamingClientConn's Receive method
|
|
||||||
// returns an error wrapping [io.EOF]. Clients should check for this using the
|
|
||||||
// standard library's [errors.Is]. If the server encounters an error during
|
|
||||||
// processing, subsequent calls to the StreamingClientConn's Send method will
|
|
||||||
// return an error wrapping [io.EOF]; clients may then call Receive to unmarshal
|
|
||||||
// the error.
|
|
||||||
//
|
|
||||||
// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for
|
|
||||||
// use by the gRPC and Connect protocols: applications may read them but
|
|
||||||
// shouldn't write them.
|
|
||||||
//
|
|
||||||
// StreamingClientConn implementations provided by this module guarantee that
|
|
||||||
// all returned errors can be cast to [*Error] using the standard library's
|
|
||||||
// [errors.As].
|
|
||||||
//
|
|
||||||
// In order to support bidirectional streaming RPCs, all StreamingClientConn
|
|
||||||
// implementations must support limited concurrent use. See the comments on
|
|
||||||
// each group of methods for details.
|
|
||||||
type StreamingClientConn interface {
|
|
||||||
// Spec and Peer must be safe to call concurrently with all other methods.
|
|
||||||
Spec() Spec
|
|
||||||
Peer() Peer
|
|
||||||
|
|
||||||
// Send, RequestHeader, and CloseRequest may race with each other, but must
|
|
||||||
// be safe to call concurrently with all other methods.
|
|
||||||
Send(any) error
|
|
||||||
RequestHeader() http.Header
|
|
||||||
CloseRequest() error
|
|
||||||
|
|
||||||
// Receive, ResponseHeader, ResponseTrailer, and CloseResponse may race with
|
|
||||||
// each other, but must be safe to call concurrently with all other methods.
|
|
||||||
Receive(any) error
|
|
||||||
ResponseHeader() http.Header
|
|
||||||
ResponseTrailer() http.Header
|
|
||||||
CloseResponse() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request is a wrapper around a generated request message. It provides
|
|
||||||
// access to metadata like headers and the RPC specification, as well as
|
|
||||||
// strongly-typed access to the message itself.
|
|
||||||
type Request[T any] struct {
|
|
||||||
Msg *T
|
|
||||||
|
|
||||||
spec Spec
|
|
||||||
peer Peer
|
|
||||||
header http.Header
|
|
||||||
method string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRequest wraps a generated request message.
|
|
||||||
func NewRequest[T any](message *T) *Request[T] {
|
|
||||||
return &Request[T]{
|
|
||||||
Msg: message,
|
|
||||||
// Initialized lazily so we don't allocate unnecessarily.
|
|
||||||
header: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any returns the concrete request message as an empty interface, so that
|
|
||||||
// *Request implements the [AnyRequest] interface.
|
|
||||||
func (r *Request[_]) Any() any {
|
|
||||||
return r.Msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec returns a description of this RPC.
|
|
||||||
func (r *Request[_]) Spec() Spec {
|
|
||||||
return r.spec
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peer describes the other party for this RPC.
|
|
||||||
func (r *Request[_]) Peer() Peer {
|
|
||||||
return r.peer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the HTTP headers for this request. Headers beginning with
|
|
||||||
// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC
|
|
||||||
// protocols: applications may read them but shouldn't write them.
|
|
||||||
func (r *Request[_]) Header() http.Header {
|
|
||||||
if r.header == nil {
|
|
||||||
r.header = make(http.Header)
|
|
||||||
}
|
|
||||||
return r.header
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPMethod returns the HTTP method for this request. This is nearly always
|
|
||||||
// POST, but side-effect-free unary RPCs could be made via a GET.
|
|
||||||
//
|
|
||||||
// On a newly created request, via NewRequest, this will return the empty
|
|
||||||
// string until the actual request is actually sent and the HTTP method
|
|
||||||
// determined. This means that client interceptor functions will see the
|
|
||||||
// empty string until *after* they delegate to the handler they wrapped. It
|
|
||||||
// is even possible for this to return the empty string after such delegation,
|
|
||||||
// if the request was never actually sent to the server (and thus no
|
|
||||||
// determination ever made about the HTTP method).
|
|
||||||
func (r *Request[_]) HTTPMethod() string {
|
|
||||||
return r.method
|
|
||||||
}
|
|
||||||
|
|
||||||
// internalOnly implements AnyRequest.
|
|
||||||
func (r *Request[_]) internalOnly() {}
|
|
||||||
|
|
||||||
// setRequestMethod sets the request method to the given value.
|
|
||||||
func (r *Request[_]) setRequestMethod(method string) {
|
|
||||||
r.method = method
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnyRequest is the common method set of every [Request], regardless of type
|
|
||||||
// parameter. It's used in unary interceptors.
|
|
||||||
//
|
|
||||||
// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for
|
|
||||||
// use by the gRPC and Connect protocols: applications may read them but
|
|
||||||
// shouldn't write them.
|
|
||||||
//
|
|
||||||
// To preserve our ability to add methods to this interface without breaking
|
|
||||||
// backward compatibility, only types defined in this package can implement
|
|
||||||
// AnyRequest.
|
|
||||||
type AnyRequest interface {
|
|
||||||
Any() any
|
|
||||||
Spec() Spec
|
|
||||||
Peer() Peer
|
|
||||||
Header() http.Header
|
|
||||||
HTTPMethod() string
|
|
||||||
|
|
||||||
internalOnly()
|
|
||||||
setRequestMethod(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response is a wrapper around a generated response message. It provides
|
|
||||||
// access to metadata like headers and trailers, as well as strongly-typed
|
|
||||||
// access to the message itself.
|
|
||||||
type Response[T any] struct {
|
|
||||||
Msg *T
|
|
||||||
|
|
||||||
header http.Header
|
|
||||||
trailer http.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewResponse wraps a generated response message.
|
|
||||||
func NewResponse[T any](message *T) *Response[T] {
|
|
||||||
return &Response[T]{
|
|
||||||
Msg: message,
|
|
||||||
// Initialized lazily so we don't allocate unnecessarily.
|
|
||||||
header: nil,
|
|
||||||
trailer: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any returns the concrete response message as an empty interface, so that
|
|
||||||
// *Response implements the [AnyResponse] interface.
|
|
||||||
func (r *Response[_]) Any() any {
|
|
||||||
return r.Msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the HTTP headers for this response. Headers beginning with
|
|
||||||
// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC
|
|
||||||
// protocols: applications may read them but shouldn't write them.
|
|
||||||
func (r *Response[_]) Header() http.Header {
|
|
||||||
if r.header == nil {
|
|
||||||
r.header = make(http.Header)
|
|
||||||
}
|
|
||||||
return r.header
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trailer returns the trailers for this response. Depending on the underlying
|
|
||||||
// RPC protocol, trailers may be sent as HTTP trailers or a protocol-specific
|
|
||||||
// block of in-body metadata.
|
|
||||||
//
|
|
||||||
// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the
|
|
||||||
// Connect and gRPC protocols: applications may read them but shouldn't write
|
|
||||||
// them.
|
|
||||||
func (r *Response[_]) Trailer() http.Header {
|
|
||||||
if r.trailer == nil {
|
|
||||||
r.trailer = make(http.Header)
|
|
||||||
}
|
|
||||||
return r.trailer
|
|
||||||
}
|
|
||||||
|
|
||||||
// internalOnly implements AnyResponse.
|
|
||||||
func (r *Response[_]) internalOnly() {}
|
|
||||||
|
|
||||||
// AnyResponse is the common method set of every [Response], regardless of type
|
|
||||||
// parameter. It's used in unary interceptors.
|
|
||||||
//
|
|
||||||
// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for
|
|
||||||
// use by the gRPC and Connect protocols: applications may read them but
|
|
||||||
// shouldn't write them.
|
|
||||||
//
|
|
||||||
// To preserve our ability to add methods to this interface without breaking
|
|
||||||
// backward compatibility, only types defined in this package can implement
|
|
||||||
// AnyResponse.
|
|
||||||
type AnyResponse interface {
|
|
||||||
Any() any
|
|
||||||
Header() http.Header
|
|
||||||
Trailer() http.Header
|
|
||||||
|
|
||||||
internalOnly()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPClient is the interface connect expects HTTP clients to implement. The
|
|
||||||
// standard library's *http.Client implements HTTPClient.
|
|
||||||
type HTTPClient interface {
|
|
||||||
Do(*http.Request) (*http.Response, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec is a description of a client call or a handler invocation.
|
|
||||||
//
|
|
||||||
// If you're using Protobuf, protoc-gen-connect-go generates a constant for the
|
|
||||||
// fully-qualified Procedure corresponding to each RPC in your schema.
|
|
||||||
type Spec struct {
|
|
||||||
StreamType StreamType
|
|
||||||
Schema any // for protobuf RPCs, a protoreflect.MethodDescriptor
|
|
||||||
Procedure string // for example, "/acme.foo.v1.FooService/Bar"
|
|
||||||
IsClient bool // otherwise we're in a handler
|
|
||||||
IdempotencyLevel IdempotencyLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peer describes the other party to an RPC.
|
|
||||||
//
|
|
||||||
// When accessed client-side, Addr contains the host or host:port from the
|
|
||||||
// server's URL. When accessed server-side, Addr contains the client's address
|
|
||||||
// in IP:port format.
|
|
||||||
//
|
|
||||||
// On both the client and the server, Protocol is the RPC protocol in use.
|
|
||||||
// Currently, it's either [ProtocolConnect], [ProtocolGRPC], or
|
|
||||||
// [ProtocolGRPCWeb], but additional protocols may be added in the future.
|
|
||||||
//
|
|
||||||
// Query contains the query parameters for the request. For the server, this
|
|
||||||
// will reflect the actual query parameters sent. For the client, it is unset.
|
|
||||||
type Peer struct {
|
|
||||||
Addr string
|
|
||||||
Protocol string
|
|
||||||
Query url.Values // server-only
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPeerForURL(url *url.URL, protocol string) Peer {
|
|
||||||
return Peer{
|
|
||||||
Addr: url.Host,
|
|
||||||
Protocol: protocol,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handlerConnCloser extends StreamingHandlerConn with a method for handlers to
|
|
||||||
// terminate the message exchange (and optionally send an error to the client).
|
|
||||||
type handlerConnCloser interface {
|
|
||||||
StreamingHandlerConn
|
|
||||||
|
|
||||||
Close(error) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// receiveConn represents the shared methods of both StreamingClientConn and StreamingHandlerConn
|
|
||||||
// that the below helper functions use for implementing the rules around a "unary" stream, that
|
|
||||||
// is expected to have exactly one message (or zero messages followed by a non-EOF error).
|
|
||||||
type receiveConn interface {
|
|
||||||
Spec() Spec
|
|
||||||
Receive(any) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasHTTPMethod is implemented by streaming connections that support HTTP methods other than
|
|
||||||
// POST.
|
|
||||||
type hasHTTPMethod interface {
|
|
||||||
getHTTPMethod() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// errStreamingClientConn is a sentinel error implementation of StreamingClientConn.
|
|
||||||
type errStreamingClientConn struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) Receive(msg any) error {
|
|
||||||
return c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) Spec() Spec {
|
|
||||||
return Spec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) Peer() Peer {
|
|
||||||
return Peer{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) Send(msg any) error {
|
|
||||||
return c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) CloseRequest() error {
|
|
||||||
return c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) CloseResponse() error {
|
|
||||||
return c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) RequestHeader() http.Header {
|
|
||||||
return make(http.Header)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) ResponseHeader() http.Header {
|
|
||||||
return make(http.Header)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *errStreamingClientConn) ResponseTrailer() http.Header {
|
|
||||||
return make(http.Header)
|
|
||||||
}
|
|
||||||
|
|
||||||
// receiveUnaryResponse unmarshals a message from a StreamingClientConn, then
|
|
||||||
// envelopes the message and attaches headers and trailers. It attempts to
|
|
||||||
// consume the response stream and isn't appropriate when receiving multiple
|
|
||||||
// messages.
|
|
||||||
func receiveUnaryResponse[T any](conn StreamingClientConn, initializer maybeInitializer) (*Response[T], error) {
|
|
||||||
msg, err := receiveUnaryMessage[T](conn, initializer, "response")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Response[T]{
|
|
||||||
Msg: msg,
|
|
||||||
header: conn.ResponseHeader(),
|
|
||||||
trailer: conn.ResponseTrailer(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// receiveUnaryRequest unmarshals a message from a StreamingClientConn, then
|
|
||||||
// envelopes the message and attaches headers and other request properties. It
|
|
||||||
// attempts to consume the request stream and isn't appropriate when receiving
|
|
||||||
// multiple messages.
|
|
||||||
func receiveUnaryRequest[T any](conn StreamingHandlerConn, initializer maybeInitializer) (*Request[T], error) {
|
|
||||||
msg, err := receiveUnaryMessage[T](conn, initializer, "request")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
method := http.MethodPost
|
|
||||||
if hasRequestMethod, ok := conn.(hasHTTPMethod); ok {
|
|
||||||
method = hasRequestMethod.getHTTPMethod()
|
|
||||||
}
|
|
||||||
return &Request[T]{
|
|
||||||
Msg: msg,
|
|
||||||
spec: conn.Spec(),
|
|
||||||
peer: conn.Peer(),
|
|
||||||
header: conn.RequestHeader(),
|
|
||||||
method: method,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func receiveUnaryMessage[T any](conn receiveConn, initializer maybeInitializer, what string) (*T, error) {
|
|
||||||
var msg T
|
|
||||||
if err := initializer.maybe(conn.Spec(), &msg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Possibly counter-intuitive, but the gRPC specs about error codes state that both clients
|
|
||||||
// and servers should return "unimplemented" when they encounter a cardinality violation: where
|
|
||||||
// the number of messages in the stream is wrong. Search for "cardinality violation" in the
|
|
||||||
// following docs:
|
|
||||||
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
|
||||||
if err := conn.Receive(&msg); err != nil {
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
err = NewError(CodeUnimplemented, fmt.Errorf("unary %s has zero messages", what))
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// In a well-formed stream, the one message must be the only content in the body.
|
|
||||||
// To verify that it is well-formed, try to read another message from the stream.
|
|
||||||
// TODO: optimize this second receive: ideally do it w/out allocation, w/out
|
|
||||||
// fully reading next message (if one is present), and w/out trying to
|
|
||||||
// actually unmarshal the bytes)
|
|
||||||
var msg2 T
|
|
||||||
if err := initializer.maybe(conn.Spec(), &msg2); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Receive(&msg2); !errors.Is(err, io.EOF) {
|
|
||||||
if err == nil {
|
|
||||||
err = NewError(CodeUnimplemented, fmt.Errorf("unary %s has multiple messages", what))
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &msg, nil
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user