mirror of
https://github.com/docker/cli.git
synced 2026-11-06 21:18:28 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6d274590f2 | ||
|
|
6998c79cfd | ||
|
|
c7bdacad70 | ||
|
|
1faf23e25f | ||
|
|
b8dd18fe2c | ||
|
|
a3539bd5c9 | ||
|
|
adfb114576 | ||
|
|
4e09976ddd | ||
|
|
69c9ccd358 | ||
|
|
1600418944 | ||
|
|
1fab379bcc | ||
|
|
df9a0f430f | ||
|
|
fa65e9a3a0 | ||
|
|
dc66c2a8d0 | ||
|
|
ae317ab44a | ||
|
|
3f2ad8bf05 | ||
|
|
6f8369014a | ||
|
|
e08aa72569 | ||
|
|
f522fffe52 | ||
|
|
5e1e84ee5d | ||
|
|
00bf2d970d | ||
|
|
4a93a41589 | ||
|
|
e300e529d5 | ||
|
|
72f6e50c62 | ||
|
|
52a2671034 | ||
|
|
1d02640576 | ||
|
|
4d74583397 | ||
|
|
de4ddc4fae | ||
|
|
ccac941dd9 | ||
|
|
e6beb40c03 | ||
|
|
dfc520dcc2 | ||
|
|
a3a2bb6118 | ||
|
|
c11295afc4 | ||
|
|
010e125d8b | ||
|
|
5cdf229230 | ||
|
|
66f6797582 | ||
|
|
cb8870a537 | ||
|
|
529f66d2f9 | ||
|
|
4d6ff7d646 | ||
|
|
b997c5b7aa | ||
|
|
1822903c1d | ||
|
|
37013cb262 | ||
|
|
61ee187552 | ||
|
|
433b13a249 | ||
|
|
5c8bfb2844 | ||
|
|
528a2ada2e | ||
|
|
de8f4032ad | ||
|
|
e4078058de | ||
|
|
8da8b00555 | ||
|
|
067d069f0c | ||
|
|
02fbbf3eb0 | ||
|
|
890976a13b | ||
|
|
23c66ccd61 | ||
|
|
3e686a8b59 |
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
// https://docs.renovatebot.com/modules/manager/github-actions/
|
||||
// https://docs.renovatebot.com/presets-helpers/
|
||||
"helpers:pinGitHubActionDigestsToSemver",
|
||||
":gitSignOff"
|
||||
],
|
||||
"gomod": {
|
||||
"managerFilePatterns": ["/(^|/)vendor\\.mod$/"]
|
||||
},
|
||||
"postUpdateOptions": ["gomodTidy"],
|
||||
"minimumReleaseAge": "3 days",
|
||||
"internalChecksFilter": "strict",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessagePrefix": "vendor:"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["cmd/docker-trust/go.mod"],
|
||||
// The trust module does not vendor dependencies; vendor/ belongs to the root module.
|
||||
"postUpdateOptions": ["gomodSkipVendor"],
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessagePrefix": "cmd/docker-trust:"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"groupName": "golang.org/x packages",
|
||||
"groupSlug": "golang-x",
|
||||
"matchDepNames": ["/^golang\\.org\\/x\\//"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"addLabels": ["area/ci", "status/2-code-review"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "docker GitHub Actions",
|
||||
"groupSlug": "docker-actions",
|
||||
"matchDepNames": ["/^docker\\//"]
|
||||
},
|
||||
{
|
||||
// Keep all the google.golang.org/genproto modules at the same version.
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"groupName": "google.golang.org/genproto packages",
|
||||
"groupSlug": "google-genproto",
|
||||
"matchDepNames": ["/^google\\.golang\\.org\\/genproto(\\/|$)/"],
|
||||
"schedule": ["every month"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -65,10 +65,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
set: |
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
|
||||
bin-image:
|
||||
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@cbc9d42f8c89a55623392aadd03600965b80e963 # v1.18.1
|
||||
permissions:
|
||||
contents: read # same as global permission
|
||||
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
|
||||
@@ -156,10 +156,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||
with:
|
||||
targets: plugins-cross
|
||||
set: |
|
||||
|
||||
@@ -64,18 +64,18 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.9"
|
||||
go-version: "1.27.3"
|
||||
cache: false
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
@@ -26,6 +26,9 @@ on:
|
||||
|
||||
jobs:
|
||||
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
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -66,7 +69,7 @@ jobs:
|
||||
docker info
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Run ${{ matrix.target }}
|
||||
run: |
|
||||
@@ -80,4 +83,4 @@ jobs:
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
use_oidc: true
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload context
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: pr-review-context
|
||||
path: context/
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
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:
|
||||
contents: read # Read repository files and PR diffs
|
||||
pull-requests: write # Post review comments
|
||||
|
||||
@@ -26,14 +26,17 @@ on:
|
||||
|
||||
jobs:
|
||||
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
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||
with:
|
||||
targets: test-coverage
|
||||
-
|
||||
@@ -41,9 +44,12 @@ jobs:
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
use_oidc: true
|
||||
|
||||
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 }}
|
||||
env:
|
||||
GOPATH: ${{ github.workspace }}
|
||||
@@ -53,9 +59,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
- macos-15-intel # macOS 15 on Intel
|
||||
- macos-15 # macOS 15 on arm64 (Apple Silicon M1)
|
||||
- macos-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
|
||||
steps:
|
||||
-
|
||||
@@ -68,7 +74,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.9"
|
||||
go-version: "1.27.3"
|
||||
cache: false
|
||||
-
|
||||
name: Test
|
||||
@@ -86,4 +92,4 @@ jobs:
|
||||
with:
|
||||
files: /tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
use_oidc: true
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Run
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.9"
|
||||
go-version: "1.27.3"
|
||||
cache: false
|
||||
-
|
||||
name: Run gocompat check
|
||||
|
||||
+1
-11
@@ -5,7 +5,7 @@ run:
|
||||
# which causes it to fallback to go1.17 semantics.
|
||||
#
|
||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
||||
go: "1.26.9"
|
||||
go: "1.27.3"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
@@ -201,11 +201,6 @@ linters:
|
||||
linters:
|
||||
- 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.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
@@ -227,11 +222,6 @@ linters:
|
||||
linters:
|
||||
- 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.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
|
||||
+9
-7
@@ -5,14 +5,16 @@ ARG BASE_VARIANT=alpine
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
ARG ALPINE_VERSION=3.24
|
||||
|
||||
ARG GO_VERSION=1.26.9
|
||||
# BASE_DEBIAN_DISTRO sets the version of the debian base image to use,
|
||||
ARG BASE_DEBIAN_DISTRO=trixie
|
||||
|
||||
ARG GO_VERSION=1.27.3
|
||||
|
||||
# XX_VERSION specifies the version of the xx utility to use.
|
||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
||||
ARG XX_VERSION=1.9.0
|
||||
ARG XX_VERSION=1.10.0
|
||||
|
||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
||||
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
|
||||
@@ -25,12 +27,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG BUILDX_VERSION=0.38.1
|
||||
|
||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG COMPOSE_VERSION=v5.1.3
|
||||
ARG COMPOSE_VERSION=v5.7.0
|
||||
|
||||
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
|
||||
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
|
||||
ARG GOVERSIONINFO_VERSION
|
||||
|
||||
@@ -30,7 +30,11 @@ package hooks
|
||||
type ResponseType int
|
||||
|
||||
const (
|
||||
// NextSteps renders suggestions under a shared "What's next:" header.
|
||||
NextSteps ResponseType = 0
|
||||
// GenericMessage renders before NextSteps, without a header or indentation.
|
||||
// CLI versions that do not recognize this type ignore the response.
|
||||
GenericMessage ResponseType = 1
|
||||
)
|
||||
|
||||
// Request is the type representing the information
|
||||
|
||||
@@ -7,17 +7,42 @@ const (
|
||||
indent = " "
|
||||
)
|
||||
|
||||
// PrintNextSteps renders list of [NextSteps] messages and writes them
|
||||
// to out. It is a no-op if messages is empty.
|
||||
func PrintNextSteps(out io.Writer, messages []string) {
|
||||
if len(messages) == 0 {
|
||||
return
|
||||
// PrintMessages groups messages by type and writes them to out. Generic messages
|
||||
// appear first, without a header or indentation, followed by next steps under a
|
||||
// shared "What's next:" header. Order within each type is preserved.
|
||||
// Messages with no lines or an unknown type produce no output.
|
||||
func PrintMessages(out io.Writer, messages []Response, evaluate func(Response) []string) {
|
||||
grouped := make(map[ResponseType][]string)
|
||||
for _, message := range messages {
|
||||
lines := evaluate(message)
|
||||
grouped[message.Type] = append(grouped[message.Type], lines...)
|
||||
}
|
||||
for _, responseType := range []ResponseType{GenericMessage, NextSteps} {
|
||||
lines := grouped[responseType]
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(out, whatsNext)
|
||||
for _, msg := range messages {
|
||||
_, _ = io.WriteString(out, indent)
|
||||
_, _ = io.WriteString(out, msg)
|
||||
_, _ = io.WriteString(out, "\n")
|
||||
header, prefix := "\n", ""
|
||||
if responseType == NextSteps {
|
||||
header, prefix = whatsNext, indent
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(out, header)
|
||||
for _, line := range lines {
|
||||
_, _ = io.WriteString(out, prefix)
|
||||
_, _ = io.WriteString(out, line)
|
||||
_, _ = io.WriteString(out, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintNextSteps renders list of [NextSteps] messages and writes them
|
||||
// to out. It is a no-op if messages is empty.
|
||||
//
|
||||
// Deprecated: use [PrintMessages] instead.
|
||||
func PrintNextSteps(out io.Writer, messages []string) {
|
||||
PrintMessages(out, []Response{{Type: NextSteps}}, func(Response) []string {
|
||||
return messages
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,85 @@ import (
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestPrintHookMessages(t *testing.T) {
|
||||
func TestPrintMessages(t *testing.T) {
|
||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
||||
|
||||
for _, tc := range []struct {
|
||||
doc string
|
||||
messages []hooks.Response
|
||||
expected string
|
||||
}{
|
||||
{doc: "no messages"},
|
||||
{
|
||||
doc: "no lines",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.GenericMessage},
|
||||
{Type: hooks.NextSteps},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "generic message",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.GenericMessage, Template: "Session active"},
|
||||
},
|
||||
expected: "\nSession active\n",
|
||||
},
|
||||
{
|
||||
doc: "preserve message formatting",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.GenericMessage, Template: "Session active\n\n Details"},
|
||||
},
|
||||
expected: "\nSession active\n\n Details\n",
|
||||
},
|
||||
{
|
||||
doc: "next steps",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.NextSteps, Template: "Try another command"},
|
||||
},
|
||||
expected: header + " Try another command\n",
|
||||
},
|
||||
{
|
||||
doc: "group interleaved types and preserve order within each type",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.NextSteps, Template: "First suggestion"},
|
||||
{Type: hooks.GenericMessage, Template: "Session active"},
|
||||
{Type: hooks.NextSteps, Template: "Second suggestion\nThird suggestion"},
|
||||
{Type: hooks.GenericMessage, Template: "Another status"},
|
||||
},
|
||||
expected: "\nSession active\nAnother status\n" + header + " First suggestion\n Second suggestion\n Third suggestion\n",
|
||||
},
|
||||
{
|
||||
doc: "unknown type",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.ResponseType(99), Template: "Ignored"},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "unknown type does not affect known messages",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.ResponseType(99), Template: "Ignored"},
|
||||
{Type: hooks.GenericMessage, Template: "Session active"},
|
||||
{Type: hooks.NextSteps, Template: "Try another command"},
|
||||
},
|
||||
expected: "\nSession active\n" + header + " Try another command\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
var w strings.Builder
|
||||
hooks.PrintMessages(&w, tc.messages, func(message hooks.Response) []string {
|
||||
if message.Template == "" {
|
||||
return nil
|
||||
}
|
||||
lines, err := hooks.ParseTemplate(message.Template, nil)
|
||||
assert.NilError(t, err)
|
||||
return lines
|
||||
})
|
||||
assert.Equal(t, w.String(), tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintNextSteps(t *testing.T) {
|
||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
||||
|
||||
tests := []struct {
|
||||
@@ -34,6 +112,11 @@ func TestPrintHookMessages(t *testing.T) {
|
||||
" Foo\n" +
|
||||
" bar\n",
|
||||
},
|
||||
{
|
||||
doc: "preserve empty and indented lines",
|
||||
messages: []string{"", " Details", ""},
|
||||
expectedOutput: header + " \n Details\n \n",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
@@ -43,7 +44,16 @@ func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
|
||||
if n := strings.Count(out, "\n"); n > maxMessages {
|
||||
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
|
||||
}
|
||||
return strings.SplitN(out, "\n", maxMessages), nil
|
||||
lines := strings.SplitN(out, "\n", maxMessages)
|
||||
|
||||
// Skip messages containing only whitespace, but preserve empty lines
|
||||
// in messages that have content (e.g., for paragraph spacing).
|
||||
if !slices.ContainsFunc(lines, func(line string) bool {
|
||||
return strings.TrimSpace(line) != ""
|
||||
}) {
|
||||
return nil, nil
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
var ErrHookTemplateParse = errors.New("failed to parse hook template")
|
||||
|
||||
@@ -33,7 +33,17 @@ func TestParseTemplate(t *testing.T) {
|
||||
{
|
||||
doc: "empty template",
|
||||
template: "",
|
||||
expectedOutput: []string{""},
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "only empty lines",
|
||||
template: "\n\n",
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "whitespace-only lines",
|
||||
template: " \n\t\n ",
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "plain message",
|
||||
|
||||
@@ -49,11 +49,18 @@ func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, sub
|
||||
}
|
||||
|
||||
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
|
||||
messages := invokeAndCollectHooks(ctx, cfg, rootCmd, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintMessages(subCommand.ErrOrStderr(), messages, func(message hooks.Response) []string {
|
||||
lines, err := hooks.ParseTemplate(message.Template, subCommand)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Debug("Failed to evaluate hook template")
|
||||
return nil
|
||||
}
|
||||
return lines
|
||||
})
|
||||
}
|
||||
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []hooks.Response {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -64,17 +71,17 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
}
|
||||
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
var collected []hooks.Response
|
||||
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (hooks.Response, error) {
|
||||
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
||||
if !matched {
|
||||
return nil, false, nil
|
||||
return hooks.Response{}, nil
|
||||
}
|
||||
|
||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return hooks.Response{}, err
|
||||
}
|
||||
|
||||
resp, err := p.RunHook(ctx, hooks.Request{
|
||||
@@ -83,29 +90,23 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
CommandError: cmdErrorMessage,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return hooks.Response{}, err
|
||||
}
|
||||
|
||||
var message hooks.Response
|
||||
if err := json.Unmarshal(resp, &message); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
return hooks.Response{}, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
}
|
||||
|
||||
// currently the only hook type
|
||||
if message.Type != hooks.NextSteps {
|
||||
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
if message.Type != hooks.NextSteps && message.Type != hooks.GenericMessage {
|
||||
return hooks.Response{}, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
}
|
||||
|
||||
messages, err = hooks.ParseTemplate(message.Template, subCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return messages, true, nil
|
||||
return message, nil
|
||||
}
|
||||
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
message, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
if err != nil {
|
||||
// skip misbehaving plugins, but don't halt execution
|
||||
logrus.WithFields(logrus.Fields{
|
||||
@@ -114,38 +115,13 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
}).Debug("Plugin hook invocation failed")
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
if message.Template == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var appended bool
|
||||
nextSteps, appended = appendNextSteps(nextSteps, messages)
|
||||
if !appended {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"plugin": pluginName,
|
||||
}).Debug("Plugin responded with an empty hook message; ignoring")
|
||||
}
|
||||
collected = append(collected, message)
|
||||
}
|
||||
return nextSteps
|
||||
}
|
||||
|
||||
// appendNextSteps appends the processed hook output to the nextSteps slice.
|
||||
// If the processed hook output is empty, it is not appended.
|
||||
// Empty lines are not stripped if there's at least one non-empty line.
|
||||
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
||||
empty := true
|
||||
for _, l := range processed {
|
||||
if strings.TrimSpace(l) != "" {
|
||||
empty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if empty {
|
||||
return nextSteps, false
|
||||
}
|
||||
|
||||
return append(nextSteps, processed...), true
|
||||
return collected
|
||||
}
|
||||
|
||||
// pluginMatch takes a plugin configuration and a string representing the
|
||||
|
||||
@@ -2,6 +2,11 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
@@ -258,38 +263,6 @@ func TestMatchHookConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendNextSteps(t *testing.T) {
|
||||
testCases := []struct {
|
||||
processed []string
|
||||
expectedOut []string
|
||||
}{
|
||||
{
|
||||
processed: []string{},
|
||||
expectedOut: []string{},
|
||||
},
|
||||
{
|
||||
processed: []string{"", ""},
|
||||
expectedOut: []string{},
|
||||
},
|
||||
{
|
||||
processed: []string{"Some hint", "", "Some other hint"},
|
||||
expectedOut: []string{"Some hint", "", "Some other hint"},
|
||||
},
|
||||
{
|
||||
processed: []string{"Hint 1", "Hint 2"},
|
||||
expectedOut: []string{"Hint 1", "Hint 2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got, appended := appendNextSteps([]string{}, tc.processed)
|
||||
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
|
||||
assert.Check(t, is.Equal(appended, len(got) > 0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
@@ -336,7 +309,7 @@ func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
|
||||
// On success, error-hooks should not match, so the plugin
|
||||
// binary is never looked up and no results are returned.
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
context.Background(), cfg, root,
|
||||
"build", map[string]string{}, "",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
@@ -349,7 +322,7 @@ func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
|
||||
root.AddCommand(sub)
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
context.Background(), cfg, root,
|
||||
"build", map[string]string{}, "some error",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
@@ -368,8 +341,108 @@ func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
|
||||
cancel() // cancel immediately
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
ctx, cfg, root, sub,
|
||||
ctx, cfg, root,
|
||||
"build", map[string]string{}, "exit status 1",
|
||||
)
|
||||
assert.Check(t, is.Nil(result))
|
||||
}
|
||||
|
||||
func TestRunHooksMessages(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("test plugins require a POSIX shell")
|
||||
}
|
||||
|
||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
||||
for _, tc := range []struct {
|
||||
doc string
|
||||
responses map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
doc: "generic message preserves formatting and expands templates",
|
||||
responses: map[string]string{
|
||||
"status": `{"Type":1,"Template":"Status for {{command}}\n\n Details"}`,
|
||||
},
|
||||
expected: "\nStatus for build\n\n Details\n",
|
||||
},
|
||||
{
|
||||
doc: "omitted type defaults to next steps",
|
||||
responses: map[string]string{
|
||||
"suggestion": `{"Template":"Try another command"}`,
|
||||
},
|
||||
expected: header + " Try another command\n",
|
||||
},
|
||||
{
|
||||
doc: "generic messages precede shared next steps header",
|
||||
responses: map[string]string{
|
||||
"statusone": `{"Type":1,"Template":"Session active"}`,
|
||||
"statustwo": `{"Type":1,"Template":"Session active"}`,
|
||||
"suggestionone": `{"Type":0,"Template":"Try another command"}`,
|
||||
"suggestiontwo": `{"Type":0,"Template":"Try another command"}`,
|
||||
},
|
||||
expected: "\nSession active\nSession active\n" + header + " Try another command\n Try another command\n",
|
||||
},
|
||||
{
|
||||
doc: "empty responses produce no output",
|
||||
responses: map[string]string{
|
||||
"status": `{"Type":1,"Template":" \n\t"}`,
|
||||
"suggestion": `{"Type":0,"Template":""}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "empty generic message does not affect next steps",
|
||||
responses: map[string]string{
|
||||
"status": `{"Type":1,"Template":""}`,
|
||||
"suggestion": `{"Type":0,"Template":"Try another command"}`,
|
||||
},
|
||||
expected: header + " Try another command\n",
|
||||
},
|
||||
{
|
||||
doc: "invalid responses do not affect other plugins",
|
||||
responses: map[string]string{
|
||||
"unknown": `{"Type":99,"Template":"Ignored"}`,
|
||||
"invalid": `{"Type":1,"Template":"{{"}`,
|
||||
"status": `{"Type":1,"Template":"Session active"}`,
|
||||
"suggestion": `{"Type":0,"Template":"Try another command"}`,
|
||||
},
|
||||
expected: "\nSession active\n" + header + " Try another command\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
cfg := configfile.New("")
|
||||
cfg.CLIPluginsExtraDirs = []string{pluginDir}
|
||||
cfg.Plugins = make(map[string]map[string]string)
|
||||
for name, response := range tc.responses {
|
||||
plugin := fmt.Sprintf(`#!/bin/sh
|
||||
case "$1" in
|
||||
docker-cli-plugin-metadata)
|
||||
printf '%%s\n' '{"SchemaVersion":"0.1.0","Vendor":"test"}'
|
||||
;;
|
||||
*)
|
||||
test "$2" = docker-cli-plugin-hooks || exit 1
|
||||
printf 'called\n' >> "$0.calls"
|
||||
printf '%%s\n' '%s'
|
||||
;;
|
||||
esac
|
||||
`, response)
|
||||
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+name), []byte(plugin), 0o755))
|
||||
cfg.Plugins[name] = map[string]string{"hooks": "build"}
|
||||
}
|
||||
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
var output strings.Builder
|
||||
sub.SetErr(&output)
|
||||
|
||||
runHooks(context.Background(), cfg, root, sub, "build", nil, "")
|
||||
assert.Equal(t, output.String(), tc.expected)
|
||||
for name := range tc.responses {
|
||||
calls, err := os.ReadFile(filepath.Join(pluginDir, "docker-"+name+".calls"))
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(calls), "called\n", "each plugin hook must run exactly once")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
@@ -32,6 +33,11 @@ import (
|
||||
// called.
|
||||
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
|
||||
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error {
|
||||
tcmd := newPluginCommand(dockerCli, plugin, meta)
|
||||
@@ -69,6 +75,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
||||
stopInstrumentation(err)
|
||||
return err
|
||||
}
|
||||
cobra.OnFinalize(func() { finalizeHook(dockerCli) })
|
||||
})
|
||||
return retErr
|
||||
}
|
||||
@@ -83,6 +90,59 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
||||
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
|
||||
// be called from the plugin's "main()" function. It initializes a new
|
||||
// [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,
|
||||
Hidden: true,
|
||||
// Suppress the global/parent PersistentPreRunE, which
|
||||
// needlessly initializes the client and tries to
|
||||
// connect to the daemon.
|
||||
// needlessly initializes the client and tries to connect to the
|
||||
// daemon.
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
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 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.
|
||||
|
||||
@@ -108,3 +108,45 @@ func TestDecoratedName(t *testing.T) {
|
||||
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"}
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,24 +146,27 @@ func (c *ContainerContext) ID() string {
|
||||
// slash (/) prefix stripped. Additional names for the container (related to the
|
||||
// legacy `--link` feature) are omitted when formatting "truncated".
|
||||
func (c *ContainerContext) Names() string {
|
||||
var b strings.Builder
|
||||
for i, n := range c.c.Names {
|
||||
name := strings.TrimPrefix(n, "/")
|
||||
if c.trunc {
|
||||
// When printing truncated, we only print a single name.
|
||||
//
|
||||
// Pick the first name that's not a legacy link (does not have
|
||||
// slashes inside the name itself (e.g., "/other-container/link")).
|
||||
// Normally this would be the first name found.
|
||||
if strings.IndexByte(name, '/') == -1 {
|
||||
if c.trunc {
|
||||
// When printing truncated, we only print a single name.
|
||||
//
|
||||
// Pick the first name that's not a legacy link (does not have
|
||||
// slashes inside the name itself (e.g., "/other-container/link")).
|
||||
// Normally this would be the first name found.
|
||||
for _, n := range c.c.Names {
|
||||
if name := strings.TrimPrefix(n, "/"); strings.IndexByte(name, '/') == -1 {
|
||||
return name
|
||||
}
|
||||
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 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString(name)
|
||||
b.WriteString(strings.TrimPrefix(n, "/"))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -48,6 +48,25 @@ func TestContainerPsContext(t *testing.T) {
|
||||
expValue: "foobar_baz",
|
||||
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"},
|
||||
trunc: true,
|
||||
@@ -257,6 +276,7 @@ func TestContainerPsContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupword // ignore duplicate "ubuntu" in test fixtures
|
||||
func TestContainerContextWrite(t *testing.T) {
|
||||
unixTime := time.Now().AddDate(0, 0, -1).Unix()
|
||||
expectedTime := time.Unix(unixTime, 0).String()
|
||||
|
||||
@@ -260,8 +260,11 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
contextDir = tempDir
|
||||
case build.ContextTypeRemote:
|
||||
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName)
|
||||
if err != nil && options.quiet {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
if err != nil {
|
||||
if options.quiet {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
}
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unable to prepare context: path %q not found", options.context)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -172,6 +174,35 @@ RUN echo hello world
|
||||
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
|
||||
}
|
||||
|
||||
// TestRunBuildRemoteContextDownloadError verifies that remote context
|
||||
// download errors are propagated.
|
||||
//
|
||||
// regression test for https://github.com/docker/cli/issues/7372
|
||||
func TestRunBuildRemoteContextDownloadError(t *testing.T) {
|
||||
t.Setenv("DOCKER_BUILDKIT", "0")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
imageBuildCalled := false
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imageBuildFunc: func(ctx context.Context, buildContext io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) {
|
||||
imageBuildCalled = true
|
||||
return client.ImageBuildResult{Body: io.NopCloser(bytes.NewReader(nil))}, nil
|
||||
},
|
||||
})
|
||||
|
||||
options := newBuildOptions()
|
||||
options.context = server.URL
|
||||
|
||||
err := runBuild(context.Background(), cli, options)
|
||||
|
||||
assert.ErrorContains(t, err, "unable to download remote context")
|
||||
assert.Assert(t, !imageBuildCalled, "ImageBuild should not be called when downloading the remote context fails")
|
||||
}
|
||||
|
||||
type fakeBuild struct {
|
||||
context *tar.Reader
|
||||
options client.ImageBuildOptions
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/cli/internal/registryclient"
|
||||
@@ -20,6 +22,23 @@ import (
|
||||
"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 {
|
||||
target string // the target manifest list name (also transaction ID)
|
||||
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.StringVar(&opts.variant, "variant", "", "Set architecture variant")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("os", completion.FromList(knownOSes...))
|
||||
_ = cmd.RegisterFlagCompletionFunc("arch", completion.FromList(knownArches...))
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -139,8 +160,15 @@ func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error {
|
||||
imageManifest.Descriptor.Platform.OSVersion = opts.osVersion
|
||||
}
|
||||
|
||||
if !isValidOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture) {
|
||||
return fmt.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
|
||||
// Validate platform syntax, without restricting OS/architecture combinations.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -58,15 +58,16 @@ func TestManifestAnnotate(t *testing.T) {
|
||||
assert.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.Flags().Set("os", "freebsd")
|
||||
cmd.Flags().Set("arch", "fake")
|
||||
cmd.Flags().Set("os-version", "1")
|
||||
cmd.Flags().Set("os-features", "feature1")
|
||||
cmd.Flags().Set("variant", "v7")
|
||||
expectedError = "manifest entry for image has unsupported os/arch combination"
|
||||
assert.NilError(t, cmd.Flags().Set("os", "freebsd"))
|
||||
assert.NilError(t, cmd.Flags().Set("arch", "<invalid>"))
|
||||
assert.NilError(t, cmd.Flags().Set("os-version", "1"))
|
||||
assert.NilError(t, cmd.Flags().Set("os-features", "feature1"))
|
||||
assert.NilError(t, cmd.Flags().Set("variant", "v7"))
|
||||
|
||||
expectedError = "manifest entry for image has invalid platform"
|
||||
assert.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
|
||||
cmd.Flags().Set("arch", "arm")
|
||||
assert.NilError(t, cmd.Flags().Set("arch", "arm"))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
|
||||
cmd = newInspectCommand(cli)
|
||||
|
||||
@@ -9,53 +9,6 @@ import (
|
||||
"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) {
|
||||
namedRef, err := reference.ParseNormalizedNamed(ref)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/docker/cli/internal/tui"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -226,7 +227,7 @@ func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, auth
|
||||
return resp.Auth.Status, err
|
||||
}
|
||||
|
||||
func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
|
||||
func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (message string, _ error) {
|
||||
// Some links documenting this:
|
||||
// - https://code.google.com/archive/p/mintty/issues/56
|
||||
// - https://github.com/docker/docker/issues/15272
|
||||
@@ -240,13 +241,12 @@ func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, de
|
||||
|
||||
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
|
||||
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" {
|
||||
var err error
|
||||
msg, err = loginWithDeviceCodeFlow(ctx, dockerCLI)
|
||||
// if the error represents a failure to initiate the device-code flow,
|
||||
// then we fallback to regular cli credentials login
|
||||
if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
|
||||
msg, err := loginWithDeviceCodeFlow(ctx, dockerCLI)
|
||||
if err == nil || !errors.Is(err, manager.ErrDeviceLoginStartFail) {
|
||||
return msg, err
|
||||
}
|
||||
|
||||
logrus.WithError(err).Debug("failed to start web-based login")
|
||||
_, _ = fmt.Fprint(dockerCLI.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -210,9 +210,9 @@ func TestResourceOptionsToResourceRequirementsSwap(t *testing.T) {
|
||||
r, err = swapOptions.ToResourceRequirements(flags)
|
||||
assert.NilError(t, err)
|
||||
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, is.Equal(*(r.MemorySwappiness), int64(23)))
|
||||
assert.Check(t, is.Equal(*r.MemorySwappiness, int64(23)))
|
||||
}
|
||||
|
||||
func TestToServiceNetwork(t *testing.T) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"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.
|
||||
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
|
||||
// so if there's some kind of issue we need to abort pretty
|
||||
// quickly.
|
||||
ctx, cancel := context.WithTimeout(ctx, exportTimeout)
|
||||
ctx, cancel := context.WithTimeout(ctx, ExportTimeout)
|
||||
defer cancel()
|
||||
|
||||
return r.ForceFlush(ctx)
|
||||
|
||||
@@ -109,7 +109,7 @@ func dockerSpanExporter(ctx context.Context, cli Cli) []sdktrace.TracerProviderO
|
||||
otel.Handle(err)
|
||||
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 {
|
||||
|
||||
@@ -85,7 +85,7 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
|
||||
return func(err error) {
|
||||
// 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(), exportTimeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), ExportTimeout)
|
||||
defer cancel()
|
||||
|
||||
duration := float64(time.Since(start)) / float64(time.Millisecond)
|
||||
|
||||
@@ -255,7 +255,6 @@ func mergeLoggingConfig(dst, src reflect.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func mergeUlimitsConfig(dst, src reflect.Value) error {
|
||||
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
|
||||
dst.Elem().Set(src.Elem())
|
||||
@@ -263,7 +262,6 @@ func mergeUlimitsConfig(dst, src reflect.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func mergeShellCommand(dst, src reflect.Value) error {
|
||||
if src.Len() != 0 {
|
||||
dst.Set(src)
|
||||
@@ -271,7 +269,6 @@ func mergeShellCommand(dst, src reflect.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func mergeServiceNetworkConfig(dst, src reflect.Value) error {
|
||||
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
|
||||
dst.Elem().FieldByName("Aliases").Set(src.Elem().FieldByName("Aliases"))
|
||||
@@ -285,7 +282,6 @@ func mergeServiceNetworkConfig(dst, src reflect.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func mergeUint64(dst, src reflect.Value) error {
|
||||
if !src.IsNil() {
|
||||
dst.Elem().Set(src.Elem())
|
||||
|
||||
@@ -27,8 +27,13 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
|
||||
}))
|
||||
cli.SetOut(outStream)
|
||||
|
||||
r, _, err := os.Pipe()
|
||||
r, w, err := os.Pipe()
|
||||
assert.NilError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = w.Close()
|
||||
_ = r.Close()
|
||||
})
|
||||
|
||||
cli.SetIn(streams.NewIn(r))
|
||||
|
||||
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
|
||||
time.Sleep(100 * time.Microsecond)
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/moby/moby/api/types/jsonstream"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client/pkg/jsonmessage"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
"github.com/theupdateframework/notary/tuf/data"
|
||||
@@ -46,7 +47,7 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *Repo
|
||||
// Count the times of calling for handleTarget,
|
||||
// if it is called more that once, that should be considered an error in a trusted push.
|
||||
cnt := 0
|
||||
handleTarget := func(msg jsonstream.JSONMessage) {
|
||||
handleTarget := func(msg jsonstream.Message) {
|
||||
cnt++
|
||||
if cnt > 1 {
|
||||
// handleTarget should only be called once. This will be treated as an error.
|
||||
@@ -78,14 +79,14 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *Repo
|
||||
default:
|
||||
// We want trust signatures to always take an explicit tag,
|
||||
// otherwise it will act as an untrusted push.
|
||||
if err := jsonstream.Display(ctx, in, ioStreams.Out()); err != nil {
|
||||
if err := displayJSONMessages(ctx, in, ioStreams.Out(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(ioStreams.Err(), "No tag specified, skipping trust metadata push")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := jsonstream.Display(ctx, in, ioStreams.Out(), jsonstream.WithAuxCallback(handleTarget)); err != nil {
|
||||
if err := displayJSONMessages(ctx, in, ioStreams.Out(), handleTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,3 +148,34 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *Repo
|
||||
_, _ = fmt.Fprintf(ioStreams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
type ctxReader struct {
|
||||
err chan error
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (r *ctxReader) Read(p []byte) (int, error) {
|
||||
select {
|
||||
case err := <-r.err:
|
||||
return 0, err
|
||||
default:
|
||||
return r.r.Read(p)
|
||||
}
|
||||
}
|
||||
|
||||
// displayJSONMessages prints the JSON messages from in to out. It returns
|
||||
// ctx.Err() if ctx is canceled, but not when in is closed.
|
||||
func displayJSONMessages(ctx context.Context, in io.Reader, out *streams.Out, auxCallback func(jsonstream.Message)) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
reader := &ctxReader{err: make(chan error, 1), r: in}
|
||||
stop := context.AfterFunc(ctx, func() { reader.err <- ctx.Err() })
|
||||
defer stop()
|
||||
|
||||
if err := jsonmessage.DisplayJSONMessagesStream(reader, out, out.FD(), out.IsTerminal(), auxCallback); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newTrustCommand)
|
||||
}
|
||||
|
||||
// newTrustCommand returns a cobra command for `trust` subcommands.
|
||||
func newTrustCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "trust",
|
||||
Short: "Manage trust on Docker images",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newRevokeCommand(dockerCLI),
|
||||
newSignCommand(dockerCLI),
|
||||
newTrustKeyCommand(dockerCLI),
|
||||
newTrustSignerCommand(dockerCLI),
|
||||
newInspectCommand(dockerCLI),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cmd/docker-trust/internal/trust"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary"
|
||||
"github.com/theupdateframework/notary/trustmanager"
|
||||
@@ -41,7 +41,7 @@ func newKeyGenerateCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
}
|
||||
|
||||
// key names can use lowercase alphanumeric + _ + - characters
|
||||
var validKeyName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
||||
var validKeyName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
||||
|
||||
// validate that all of the key names are unique and are alphanumeric + _ + -
|
||||
// and that we do not already have public key files in the target dir on disk
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cmd/docker-trust/internal/trust"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
@@ -45,7 +45,7 @@ func newSignerAddCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
var validSignerName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
||||
var validSignerName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
||||
|
||||
func addSigner(ctx context.Context, dockerCLI command.Cli, options signerAddOptions) error {
|
||||
signerName := options.signer
|
||||
|
||||
+16
-3
@@ -571,11 +571,24 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
return nil
|
||||
}
|
||||
if !errdefs.IsNotFound(err) {
|
||||
// For plugin not found we fall through to
|
||||
// cmd.Execute() which deals with reporting
|
||||
// "command not found" in a consistent way.
|
||||
return err
|
||||
}
|
||||
if args[0] == "offload" {
|
||||
// Report this before cmd.ExecuteContext(): plugin-specific flags such as
|
||||
// --cpus otherwise fail parsing before the root command's RunE.
|
||||
return errors.New("docker: to use 'docker offload', install the plugin.\nSee https://docs.docker.com/go/cloud-flag/")
|
||||
}
|
||||
// Other missing or invalid plugins fall through to cmd.ExecuteContext(),
|
||||
// which reports the standard unknown-command error.
|
||||
//
|
||||
// Cobra handles "--version" before it checks for unknown commands,
|
||||
// so unset it to make "docker nonexistent --version" fail instead
|
||||
// of printing the version. A global "--version" flag (as in
|
||||
// "docker --version nonexistent") was already parsed, and still
|
||||
// prints the version.
|
||||
if !cmd.Flags().Changed("version") {
|
||||
cmd.Version = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4156,6 +4156,7 @@ _docker_manifest_annotate() {
|
||||
amd64
|
||||
arm
|
||||
arm64
|
||||
loong64
|
||||
mips64
|
||||
mips64le
|
||||
ppc64le
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# 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.
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG ALPINE_VERSION=3.24
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS gen
|
||||
RUN apk add --no-cache bash git
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.9
|
||||
ARG GO_VERSION=1.27.3
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG ALPINE_VERSION=3.24
|
||||
|
||||
# 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
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG BUILDX_VERSION=0.38.1
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.9
|
||||
ARG GO_VERSION=1.27.3
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG ALPINE_VERSION=3.24
|
||||
# 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
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.9
|
||||
ARG GO_VERSION=1.27.3
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG MODOUTDATED_VERSION=v0.8.0
|
||||
ARG ALPINE_VERSION=3.24
|
||||
ARG MODOUTDATED_VERSION=v0.9.0
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
ENV GOTOOLCHAIN=local
|
||||
|
||||
@@ -44,8 +44,8 @@ created. Supported `Dockerfile` instructions:
|
||||
$ docker ps
|
||||
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
c3f279d17e0a ubuntu:24.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
|
||||
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
||||
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
||||
|
||||
$ docker commit c3f279d17e0a svendowideit/testimage:version3
|
||||
|
||||
@@ -63,8 +63,8 @@ svendowideit/testimage version3 f5283438590d 16 sec
|
||||
$ docker ps
|
||||
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
c3f279d17e0a ubuntu:24.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
|
||||
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
||||
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
||||
|
||||
$ docker inspect -f "{{ .Config.Env }}" c3f279d17e0a
|
||||
|
||||
@@ -85,8 +85,8 @@ $ docker inspect -f "{{ .Config.Env }}" f5283438590d
|
||||
$ docker ps
|
||||
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
c3f279d17e0a ubuntu:24.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
|
||||
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
||||
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
||||
|
||||
$ 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
|
||||
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
|
||||
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
||||
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
|
||||
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
|
||||
```
|
||||
|
||||
@@ -33,7 +33,7 @@ Running `docker ps --no-trunc` showing 2 linked containers.
|
||||
$ docker ps --no-trunc
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
@@ -240,13 +240,13 @@ CONTAINER ID IMAGE COMMAND CREATED
|
||||
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
|
||||
$ docker ps --filter ancestor=ubuntu:24.04
|
||||
$ docker ps --filter ancestor=ubuntu:26.04
|
||||
|
||||
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
|
||||
@@ -256,7 +256,7 @@ that have this layer in its layer stack.
|
||||
$ docker ps --filter ancestor=d0e008c6cf02
|
||||
|
||||
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
|
||||
|
||||
@@ -66,18 +66,18 @@ docker.io/library/debian:latest
|
||||
Docker images can consist of multiple layers. In the example above, the image
|
||||
consists of a single layer; `e756f3fdd6a3`.
|
||||
|
||||
Layers can be reused by images. For example, the `debian:bookworm` image shares
|
||||
its layer with the `debian:latest`. Pulling the `debian:bookworm` image therefore
|
||||
Layers can be reused by images. For example, the `debian:trixie` image shares
|
||||
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
|
||||
locally:
|
||||
|
||||
```console
|
||||
$ docker image pull debian:bookworm
|
||||
$ docker image pull debian:trixie
|
||||
|
||||
bookworm: Pulling from library/debian
|
||||
trixie: Pulling from library/debian
|
||||
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
|
||||
Status: Downloaded newer image for debian:bookworm
|
||||
docker.io/library/debian:bookworm
|
||||
Status: Downloaded newer image for debian:trixie
|
||||
docker.io/library/debian:trixie
|
||||
```
|
||||
|
||||
To see which images are present locally, use the [`docker images`](image_ls.md)
|
||||
@@ -87,13 +87,13 @@ command:
|
||||
$ docker images
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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,
|
||||
`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,
|
||||
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
|
||||
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.
|
||||
For example, `docker pull ubuntu:24.04` pulls the latest version of the Ubuntu
|
||||
24.04 image.
|
||||
For example, `docker pull ubuntu:26.04` pulls the latest version of the Ubuntu
|
||||
26.04 image.
|
||||
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
$ 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
|
||||
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
|
||||
Status: Downloaded newer image for ubuntu:24.04
|
||||
docker.io/library/ubuntu:24.04
|
||||
Status: Downloaded newer image for ubuntu:26.04
|
||||
docker.io/library/ubuntu:26.04
|
||||
```
|
||||
|
||||
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
|
||||
$ docker image ls --filter reference=ubuntu
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
ubuntu 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||
ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||
ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
|
||||
ubuntu 24.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||
ubuntu noble 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||
ubuntu 26.04 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
|
||||
|
||||
@@ -180,13 +180,12 @@ code, and authenticate.
|
||||
```console
|
||||
$ docker login
|
||||
|
||||
USING WEB-BASED LOGIN
|
||||
To sign in with credentials on the command line, use 'docker login -u <username>'
|
||||
Using web-based login. To sign in with credentials on the command line, use 'docker login -u <username>'
|
||||
|
||||
Your one-time device confirmation code is: LNFR-PGCJ
|
||||
Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate
|
||||
Open this URL to authenticate: https://login.docker.com/activate?user_code=LNFR-PGCJ
|
||||
|
||||
Waiting for authentication in the browser…
|
||||
Press ENTER to open the URL in your browser, or Ctrl+C to cancel.
|
||||
```
|
||||
|
||||
After entering the code in your browser, you are authenticated to Docker Hub
|
||||
|
||||
@@ -706,7 +706,7 @@ follows:
|
||||
| `node.platform.os` | Node operating system | `node.platform.os==windows` |
|
||||
| `node.platform.arch` | Node architecture | `node.platform.arch==x86_64` |
|
||||
| `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,
|
||||
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'
|
||||
|
||||
2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
|
||||
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu: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:26.04)
|
||||
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)
|
||||
|
||||
|
||||
+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
|
||||
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
|
||||
|
||||
@@ -406,14 +406,14 @@ We have four ways to set user memory usage:
|
||||
Examples:
|
||||
|
||||
```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
|
||||
as much memory and swap memory as they need.
|
||||
|
||||
```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
|
||||
@@ -421,7 +421,7 @@ the container can use 300M memory and as much swap memory as they need (if the
|
||||
host supports swap memory).
|
||||
|
||||
```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
|
||||
@@ -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.
|
||||
|
||||
```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
|
||||
@@ -456,7 +456,7 @@ The following example limits the memory (`-m`) to 500M and sets the memory
|
||||
reservation to 200M.
|
||||
|
||||
```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
|
||||
@@ -466,7 +466,7 @@ memory below 200M.
|
||||
The following example set memory reservation to 1G without a hard memory limit.
|
||||
|
||||
```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
|
||||
@@ -484,13 +484,13 @@ The following example limits the memory to 100M and disables the OOM killer for
|
||||
this container:
|
||||
|
||||
```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:
|
||||
|
||||
```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
|
||||
@@ -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:
|
||||
|
||||
```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
|
||||
@@ -561,7 +561,7 @@ And usually `--cpu-period` should work with `--cpu-quota`.
|
||||
Examples:
|
||||
|
||||
```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.
|
||||
@@ -582,13 +582,13 @@ We can set cpus in which to allow execution for containers.
|
||||
Examples:
|
||||
|
||||
```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.
|
||||
|
||||
```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.
|
||||
@@ -599,14 +599,14 @@ on NUMA systems.
|
||||
Examples:
|
||||
|
||||
```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
|
||||
memory nodes 1 and 3.
|
||||
|
||||
```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
|
||||
@@ -637,8 +637,8 @@ For example, the commands below create two containers with different blkio
|
||||
weight:
|
||||
|
||||
```console
|
||||
$ docker run -it --name c1 --blkio-weight 300 ubuntu:24.04 /bin/bash
|
||||
$ docker run -it --name c2 --blkio-weight 600 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:26.04 /bin/bash
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```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
|
||||
|
||||
$ 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
|
||||
|
||||
@@ -62,6 +62,22 @@ func TestNonexistingHelp(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestNonexistingVersion ensures correct behaviour when invoking a
|
||||
// nonexistent plugin with `--version`.
|
||||
func TestNonexistingVersion(t *testing.T) {
|
||||
run, _, cleanup := prepare(t)
|
||||
defer cleanup()
|
||||
|
||||
for _, flag := range []string{"--version", "-v"} {
|
||||
res := icmd.RunCmd(run("nonexistent", flag))
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 1,
|
||||
Out: icmd.None,
|
||||
})
|
||||
golden.Assert(t, res.Stderr(), "docker-nonexistent-err.golden")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunBad ensures correct behaviour when running an existent but invalid plugin
|
||||
func TestRunBad(t *testing.T) {
|
||||
run, _, cleanup := prepare(t)
|
||||
|
||||
@@ -30,5 +30,5 @@ func TestOauthLogin(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
|
||||
output, _ := io.ReadAll(p)
|
||||
assert.Check(t, strings.Contains(string(output), "USING WEB-BASED LOGIN"), string(output))
|
||||
assert.Check(t, strings.Contains(string(output), "Using web-based login"), string(output))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
@@ -17,8 +18,6 @@ import (
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
)
|
||||
|
||||
@@ -83,19 +82,18 @@ var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login
|
||||
// The retrieved tokens are stored in the credentials store (under a separate
|
||||
// key), and the refresh token is concatenated with the client ID.
|
||||
func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.AuthConfig, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
state, err := m.api.GetDeviceCode(ctx, m.audience)
|
||||
if err != nil {
|
||||
logrus.Debugf("failed to start device code login: %v", err)
|
||||
return nil, ErrDeviceLoginStartFail
|
||||
return nil, fmt.Errorf("%w: %w", ErrDeviceLoginStartFail, err)
|
||||
}
|
||||
|
||||
if state.UserCode == "" {
|
||||
logrus.Debugf("failed to start device code login: missing user code")
|
||||
return nil, ErrDeviceLoginStartFail
|
||||
return nil, fmt.Errorf("%w: missing user code", ErrDeviceLoginStartFail)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB-BASED LOGIN"))
|
||||
|
||||
var out tui.Output
|
||||
switch stream := w.(type) {
|
||||
case *streams.Out:
|
||||
@@ -103,9 +101,10 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut
|
||||
default:
|
||||
out = tui.NewOutput(streams.NewOut(w))
|
||||
}
|
||||
out.PrintNote("To sign in with credentials on the command line, use 'docker login -u <username>'\n")
|
||||
_, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode)
|
||||
_, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0])
|
||||
out.PrintNote("Using web-based login. To sign in with credentials on the command line, use 'docker login -u <username>'\n")
|
||||
_, _ = fmt.Fprintln(w, "Your one-time device confirmation code is:", aec.Bold.Apply(state.UserCode))
|
||||
_, _ = fmt.Fprintln(w, "Open this URL to authenticate:", aec.Underline.Apply(state.VerificationURI))
|
||||
_, _ = fmt.Fprintln(w, "\n"+aec.Bold.Apply("Press ENTER")+" to open the URL in your browser, or "+aec.Bold.Apply("Ctrl+C")+" to cancel.")
|
||||
|
||||
tokenResChan := make(chan api.TokenResponse)
|
||||
waitForTokenErrChan := make(chan error)
|
||||
@@ -120,11 +119,25 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut
|
||||
|
||||
go func() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
_, _ = reader.ReadString('\n')
|
||||
_ = m.openBrowser(state.VerificationURI)
|
||||
if _, err := reader.ReadString('\n'); err != nil || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
err := m.openBrowser(state.VerificationURI)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
_, _ = fmt.Fprintln(w, "No browser found. Open the URL above manually to continue.")
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(w, "Failed to open browser:", err)
|
||||
_, _ = fmt.Fprintln(w, "Open the URL above manually to continue.")
|
||||
}
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintln(w, "\nOpening browser…")
|
||||
}()
|
||||
|
||||
_, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n")
|
||||
var tokenRes api.TokenResponse
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -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
|
||||
base := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: endpoint.TLSConfig,
|
||||
DisableKeepAlives: true,
|
||||
|
||||
@@ -27,8 +27,13 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
|
||||
}))
|
||||
cli.SetOut(outStream)
|
||||
|
||||
r, _, err := os.Pipe()
|
||||
r, w, err := os.Pipe()
|
||||
assert.NilError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = w.Close()
|
||||
_ = r.Close()
|
||||
})
|
||||
|
||||
cli.SetIn(streams.NewIn(r))
|
||||
|
||||
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
|
||||
time.Sleep(100 * time.Microsecond)
|
||||
|
||||
|
||||
+14
-6
@@ -83,16 +83,24 @@ set as the **URL**, the repository is cloned locally and then sent as the contex
|
||||
Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip.
|
||||
The **--add-host** option can be set multiple times.
|
||||
|
||||
**--build-arg** *variable*
|
||||
name and value of a **buildarg**.
|
||||
**--build-arg** *name*[=*value*]
|
||||
Set a build-time variable. The *name* is the variable name, as declared
|
||||
by an `ARG` instruction in the Dockerfile. If *value* is omitted, the
|
||||
value of the variable *name* from the local environment is used; if it is
|
||||
not set there either, the default from the `ARG` instruction (if any) is
|
||||
used.
|
||||
|
||||
Build-time variables are available as environment variables to commands
|
||||
run by the Dockerfile's `RUN` instructions, and can be used for variable
|
||||
expansion in other Dockerfile instructions. An environment variable defined
|
||||
by an `ENV` instruction always overrides an `ARG` of the same name. This
|
||||
mechanism is not meant for passing secret values.
|
||||
|
||||
For example, if you want to pass a value for `http_proxy`, use
|
||||
`--build-arg=http_proxy="http://some.proxy.url"`
|
||||
|
||||
Users pass these values at build-time. Docker uses the `buildargs` as the
|
||||
environment context for command(s) run via the Dockerfile's `RUN` instruction
|
||||
or for variable expansion in other Dockerfile instructions. This is not meant
|
||||
for passing secret values. [Read more about the buildargs instruction](https://docs.docker.com/engine/reference/builder/#arg)
|
||||
See also https://docs.docker.com/reference/dockerfile/#arg and
|
||||
https://docs.docker.com/reference/cli/docker/buildx/build/#build-arg.
|
||||
|
||||
**--cache-from** ""
|
||||
Set image that will be used as a build cache source.
|
||||
|
||||
@@ -48,10 +48,10 @@ Valid placeholders for the Go template are listed below:
|
||||
|
||||
$ docker container ls -a
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
+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
|
||||
(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
|
||||
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.
|
||||
|
||||
## 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
|
||||
consists of a single layer; `e756f3fdd6a3`.
|
||||
|
||||
Layers can be reused by images. For example, the `debian:bookworm` image shares
|
||||
its layer with the `debian:latest`. Pulling the `debian:bookworm` image therefore
|
||||
Layers can be reused by images. For example, the `debian:trixie` image shares
|
||||
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
|
||||
locally:
|
||||
|
||||
$ docker image pull debian:bookworm
|
||||
$ docker image pull debian:trixie
|
||||
|
||||
bookworm: Pulling from library/debian
|
||||
trixie: Pulling from library/debian
|
||||
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
|
||||
Status: Downloaded newer image for debian:bookworm
|
||||
docker.io/library/debian:bookworm
|
||||
Status: Downloaded newer image for debian:trixie
|
||||
docker.io/library/debian:trixie
|
||||
|
||||
To see which images are present locally, use the **docker-images(1)**
|
||||
command:
|
||||
@@ -45,12 +45,12 @@ command:
|
||||
$ docker images
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
`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,
|
||||
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
|
||||
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.
|
||||
For example, `docker image pull ubuntu:24.04` pulls the latest version of the Ubuntu
|
||||
24.04 image.
|
||||
For example, `docker image pull ubuntu:26.04` pulls the latest version of the Ubuntu
|
||||
26.04 image.
|
||||
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
|
||||
Status: Downloaded newer image for ubuntu:24.04
|
||||
docker.io/library/ubuntu:24.04
|
||||
Status: Downloaded newer image for ubuntu:26.04
|
||||
docker.io/library/ubuntu:26.04
|
||||
|
||||
Docker prints the digest of the image after the pull has finished. In the example
|
||||
above, the digest of the image is:
|
||||
@@ -159,11 +159,11 @@ images that are present locally:
|
||||
|
||||
$ docker image ls --filter reference=ubuntu
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
ubuntu 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||
ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||
ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
|
||||
ubuntu 24.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||
ubuntu noble 8a3cdc4d1ad3 3 weeks ago 77.9MB
|
||||
ubuntu 26.04 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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
# docker events --since '3m'
|
||||
2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu:24.04) die
|
||||
2015-05-12T15:52:12.999999999Z07:00 4386fb97867d: (from ubuntu:24.04) stop
|
||||
2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu:26.04) die
|
||||
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: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
|
||||
|
||||
$ 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)
|
||||
|
||||
$ docker events --filter 'image=ubuntu:24.04'
|
||||
2014-05-10T17:42:14.999999999Z07:00 container start 4386fb97867d (image=ubuntu:24.04)
|
||||
2014-05-10T17:42:14.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
|
||||
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
|
||||
$ docker events --filter 'image=ubuntu:26.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:26.04)
|
||||
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04)
|
||||
|
||||
$ docker events --filter 'container=7805c1d35632'
|
||||
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)
|
||||
|
||||
$ docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d'
|
||||
2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
|
||||
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu: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:26.04)
|
||||
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)
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ Server:
|
||||
seccomp
|
||||
Profile: builtin
|
||||
cgroupns
|
||||
Kernel Version: 5.15.0-25-generic
|
||||
Operating System: Ubuntu 22.04 LTS
|
||||
Kernel Version: 6.8.0-31-generic
|
||||
Operating System: Ubuntu 24.04 LTS
|
||||
OSType: linux
|
||||
Architecture: x86_64
|
||||
CPUs: 1
|
||||
|
||||
@@ -88,13 +88,20 @@ if [ "$CGO_ENABLED" = "1" ] && [ "$(go env GOOS)" != "windows" ]; then
|
||||
fi
|
||||
export GO_BUILDMODE
|
||||
|
||||
GO_LDFLAGS="${GO_LDFLAGS:-}"
|
||||
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}\""
|
||||
fi
|
||||
# Omit DWARF debug information to reduce binary size, but retain the symbol
|
||||
# table for tools that inspect Go binaries. Pass GO_LDFLAGS="-w=0" to retain DWARF.
|
||||
GO_LDFLAGS="-w ${GO_LDFLAGS:-}"
|
||||
|
||||
# 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}\""
|
||||
fi
|
||||
}
|
||||
if [ "$CGO_ENABLED" = "1" ] && [ "$GO_LINKMODE" = "static" ] && [ "$(go env GOOS)" = "linux" ]; then
|
||||
GO_LDFLAGS="$GO_LDFLAGS -linkmode external -extldflags -static"
|
||||
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
|
||||
# llvm-strip: error: unsupported load command (cmd=0x5)
|
||||
# more info: https://github.com/docker/cli/pull/3717
|
||||
GO_LDFLAGS="$GO_LDFLAGS -s -w"
|
||||
GO_LDFLAGS="$GO_LDFLAGS -s"
|
||||
fi
|
||||
export GO_LDFLAGS="$GO_LDFLAGS" # https://github.com/koalaman/shellcheck/issues/2064
|
||||
|
||||
|
||||
+29
-28
@@ -21,11 +21,11 @@ require (
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli-docs-tool v0.11.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.9.9
|
||||
github.com/docker/docker-credential-helpers v0.9.12
|
||||
github.com/docker/go-connections v0.8.2
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/fvbommel/sortorder v1.2.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.5
|
||||
github.com/fvbommel/sortorder v1.2.1
|
||||
github.com/go-jose/go-jose/v4 v4.1.7
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
@@ -33,12 +33,12 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-runewidth v0.0.31
|
||||
github.com/moby/go-archive v0.3.3
|
||||
github.com/moby/moby/api v1.56.1
|
||||
github.com/moby/moby/client v0.6.2
|
||||
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131
|
||||
github.com/moby/moby/client v0.6.3-0.20261106011722-402d0a16f131
|
||||
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/capability v0.4.0
|
||||
github.com/moby/sys/capability v0.5.0
|
||||
github.com/moby/sys/sequential v0.7.0
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/sys/symlink v0.3.0
|
||||
@@ -52,26 +52,26 @@ require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
|
||||
go.opentelemetry.io/otel v1.46.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
|
||||
go.opentelemetry.io/otel/metric v1.46.0
|
||||
go.opentelemetry.io/otel/sdk v1.46.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0
|
||||
go.opentelemetry.io/otel/trace v1.46.0
|
||||
go.yaml.in/yaml/v3 v3.0.5
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/term v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0
|
||||
go.opentelemetry.io/otel v1.47.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0
|
||||
go.opentelemetry.io/otel/metric v1.47.0
|
||||
go.opentelemetry.io/otel/sdk v1.47.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0
|
||||
go.opentelemetry.io/otel/trace v1.47.0
|
||||
go.yaml.in/yaml/v3 v3.0.6
|
||||
golang.org/x/sync v0.24.0
|
||||
golang.org/x/sys v0.49.1-0.20261022125458-f5cb0306ac0e // includes fix for https://go.dev/issue/82324
|
||||
golang.org/x/term v0.47.0
|
||||
golang.org/x/text v0.43.0
|
||||
gotest.tools/v3 v3.5.2
|
||||
tags.cncf.io/container-device-interface v1.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0 // indirect; see https://github.com/microsoft/hcsshim/pull/2545
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261013165250-51075c6ea6bc // indirect
|
||||
github.com/Microsoft/go-winio v0.6.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -87,7 +87,7 @@ require (
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.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/sys/user v0.4.1 // indirect
|
||||
github.com/moby/sys/userns v0.2.1 // indirect
|
||||
@@ -99,13 +99,14 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
go.etcd.io/raft/v3 v3.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.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
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/net v0.60.0 // indirect
|
||||
golang.org/x/mod v0.42.0 // indirect
|
||||
golang.org/x/net v0.62.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/rpc 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-20260928230214-8a89bd6388cc // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
)
|
||||
|
||||
+60
-60
@@ -2,10 +2,10 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
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/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-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
|
||||
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0 h1:71VU8kW/LCxJkx41+YYys1EgQO+AUnPa1TrxTSyYUCU=
|
||||
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261013165250-51075c6ea6bc h1:58s5YzGPNAP7+e1uemaL0ax/NPSeJOHdRpRIowd+JDk=
|
||||
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/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
@@ -35,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/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w=
|
||||
github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
github.com/docker/docker-credential-helpers v0.9.12 h1:hAgjJcMuC17DPf0Kl/HQSZQ7cxVo+wEA8Oo1SzkmqFg=
|
||||
github.com/docker/docker-credential-helpers v0.9.12/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/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0=
|
||||
@@ -49,10 +49,10 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/fvbommel/sortorder v1.2.0 h1:TRIiRiGX+djh3Yf4FVxmWmAcYfIr5dH0NbzJWOSAWZk=
|
||||
github.com/fvbommel/sortorder v1.2.0/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.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/fvbommel/sortorder v1.2.1 h1:ASc85RbthwLEqzuiCk7WQQiopfyGWQiEYZVJ3Qa4A+U=
|
||||
github.com/fvbommel/sortorder v1.2.1/go.mod h1:LbhO04ijZIeUuvz9B9BkI/qYrpZZEn1gWhxv4QjUKVs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.7 h1:Nac/98uT5LPkfbiFuYa8ktf62a2VxwQ4xbq1GK8pV5E=
|
||||
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.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -78,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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.2 h1:v5tiZvm1KGLyg5d5oXdXe3lsSPrbEUiKoXGia//I7Qo=
|
||||
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/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo=
|
||||
@@ -88,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/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
|
||||
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
|
||||
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4=
|
||||
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
|
||||
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131 h1:tPz9ahj8F7AjQw62XQFP65kmoxF3rXTYSit3q06/yQc=
|
||||
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131/go.mod h1:pRZ1QRtx6AVhdckeFYo4DUX4ASNF0SCllhyY6aj2mgg=
|
||||
github.com/moby/moby/client v0.6.3-0.20261106011722-402d0a16f131 h1:ReIO7lalQVE4BetZuIvrxScg8Rxv4Sk74XRs06ryRDU=
|
||||
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/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
|
||||
github.com/moby/swarmkit/v2 v2.1.2/go.mod h1:GQ6T0ij2oBbWX10OHwpvK449xfXkQMZ8J+B+eQ4mgp4=
|
||||
github.com/moby/swarmkit/v2 v2.1.3 h1:UcwtwkIRjr75qSgUWCTIuzKUzAXlidxsiqVMnyxSF1w=
|
||||
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/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
|
||||
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
|
||||
github.com/moby/sys/capability v0.5.0 h1:Bm4oD9xGCtZguPnaoQy1/jvKTSwZHMvM0FrnxNYtGYU=
|
||||
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/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
@@ -155,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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
|
||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
|
||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
||||
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
|
||||
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
|
||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
||||
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.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw=
|
||||
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
|
||||
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 h1:LaLVGJtIzHcvHjVs2OftmKcc+M9mdOqS8vZ1fBD1Yes=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0/go.mod h1:vF6H5sNDuQBJWgSX00iZgbIqk2MZZoVMH0ar5t5bOpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc=
|
||||
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
|
||||
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
|
||||
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
|
||||
go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
|
||||
go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
|
||||
go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
|
||||
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/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v3 v3.0.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
|
||||
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
|
||||
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-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.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||
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-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.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.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-20190412213103-97732733099d/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.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/sys v0.49.1-0.20261022125458-f5cb0306ac0e h1:y3PdhEKVIFYc4c6gNZPEC7qPIoa9uH/2zIQEc0mw4R4=
|
||||
golang.org/x/sys v0.49.1-0.20261022125458-f5cb0306ac0e/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||
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.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -224,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=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
|
||||
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-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
|
||||
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-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
|
||||
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/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
@@ -235,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=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
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/go.mod h1:S1PSJWYPD4Iom0/39mvr/VVFCfG0Yt14j20d0OYrY2M=
|
||||
|
||||
+7
-5
@@ -1,5 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
import "slices"
|
||||
|
||||
type csiEntryState struct {
|
||||
baseState
|
||||
}
|
||||
@@ -13,11 +15,11 @@ func (csiState csiEntryState) Handle(b byte) (s state, e error) {
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(alphabetics, b):
|
||||
case slices.Contains(alphabetics, b):
|
||||
return csiState.parser.ground, nil
|
||||
case sliceContains(csiCollectables, b):
|
||||
case slices.Contains(csiCollectables, b):
|
||||
return csiState.parser.csiParam, nil
|
||||
case sliceContains(executors, b):
|
||||
case slices.Contains(executors, b):
|
||||
return csiState, csiState.parser.execute()
|
||||
}
|
||||
|
||||
@@ -33,9 +35,9 @@ func (csiState csiEntryState) Transition(s state) error {
|
||||
return csiState.parser.csiDispatch()
|
||||
case csiState.parser.csiParam:
|
||||
switch {
|
||||
case sliceContains(csiParams, csiState.parser.context.currentChar):
|
||||
case slices.Contains(csiParams, csiState.parser.context.currentChar):
|
||||
_ = csiState.parser.collectParam()
|
||||
case sliceContains(intermeds, csiState.parser.context.currentChar):
|
||||
case slices.Contains(intermeds, csiState.parser.context.currentChar):
|
||||
_ = csiState.parser.collectInter()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
import "slices"
|
||||
|
||||
type csiParamState struct {
|
||||
baseState
|
||||
}
|
||||
@@ -13,11 +15,11 @@ func (csiState csiParamState) Handle(b byte) (s state, e error) {
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(alphabetics, b):
|
||||
case slices.Contains(alphabetics, b):
|
||||
return csiState.parser.ground, nil
|
||||
case sliceContains(csiCollectables, b):
|
||||
case slices.Contains(csiCollectables, b):
|
||||
return csiState, csiState.parser.collectParam()
|
||||
case sliceContains(executors, b):
|
||||
case slices.Contains(executors, b):
|
||||
return csiState, csiState.parser.execute()
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
import "slices"
|
||||
|
||||
type escapeIntermediateState struct {
|
||||
baseState
|
||||
}
|
||||
@@ -12,11 +14,11 @@ func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(intermeds, b):
|
||||
case slices.Contains(intermeds, b):
|
||||
return escState, escState.parser.collectInter()
|
||||
case sliceContains(executors, b):
|
||||
case slices.Contains(executors, b):
|
||||
return escState, escState.parser.execute()
|
||||
case sliceContains(escapeIntermediateToGroundBytes, b):
|
||||
case slices.Contains(escapeIntermediateToGroundBytes, b):
|
||||
return escState.parser.ground, nil
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
import "slices"
|
||||
|
||||
type escapeState struct {
|
||||
baseState
|
||||
}
|
||||
@@ -16,11 +18,11 @@ func (escState escapeState) Handle(b byte) (s state, e error) {
|
||||
return escState.parser.csiEntry, nil
|
||||
case b == ANSI_OSC_STRING_ENTRY:
|
||||
return escState.parser.oscString, nil
|
||||
case sliceContains(executors, b):
|
||||
case slices.Contains(executors, b):
|
||||
return escState, escState.parser.execute()
|
||||
case sliceContains(escapeToGroundBytes, b):
|
||||
case slices.Contains(escapeToGroundBytes, b):
|
||||
return escState.parser.ground, nil
|
||||
case sliceContains(intermeds, b):
|
||||
case slices.Contains(intermeds, b):
|
||||
return escState.parser.escapeIntermediate, nil
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -1,5 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
import "slices"
|
||||
|
||||
type groundState struct {
|
||||
baseState
|
||||
}
|
||||
@@ -13,10 +15,10 @@ func (gs groundState) Handle(b byte) (s state, e error) {
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(printables, b):
|
||||
case slices.Contains(printables, b):
|
||||
return gs, gs.parser.print()
|
||||
|
||||
case sliceContains(executors, b):
|
||||
case slices.Contains(executors, b):
|
||||
return gs, gs.parser.execute()
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -20,12 +20,12 @@ type AnsiParser struct {
|
||||
oscString state
|
||||
stateMap []state
|
||||
|
||||
logf func(string, ...interface{})
|
||||
logf func(string, ...any)
|
||||
}
|
||||
|
||||
type Option func(*AnsiParser)
|
||||
|
||||
func WithLogf(f func(string, ...interface{})) Option {
|
||||
func WithLogf(f func(string, ...any)) Option {
|
||||
return func(ap *AnsiParser) {
|
||||
ap.logf = f
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Opti
|
||||
logger := log.New(logFile, "", log.LstdFlags)
|
||||
if ap.logf != nil {
|
||||
l := ap.logf
|
||||
ap.logf = func(s string, v ...interface{}) {
|
||||
ap.logf = func(s string, v ...any) {
|
||||
l(s, v...)
|
||||
logger.Printf(s, v...)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Opti
|
||||
}
|
||||
|
||||
if ap.logf == nil {
|
||||
ap.logf = func(string, ...interface{}) {}
|
||||
ap.logf = func(string, ...any) {}
|
||||
}
|
||||
|
||||
ap.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: ap}}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ func getInts(params []string, minCount int, dflt int) []int {
|
||||
|
||||
if len(ints) < minCount {
|
||||
remaining := minCount - len(ints)
|
||||
for i := 0; i < remaining; i++ {
|
||||
for range remaining {
|
||||
ints = append(ints, dflt)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,5 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
import "slices"
|
||||
|
||||
type state interface {
|
||||
Enter() error
|
||||
Exit() error
|
||||
@@ -32,7 +34,7 @@ func (base baseState) Handle(b byte) (s state, e error) {
|
||||
return base.parser.escape, nil
|
||||
case b == OSC_STRING:
|
||||
return base.parser.oscString, nil
|
||||
case sliceContains(toGroundBytes, b):
|
||||
case slices.Contains(toGroundBytes, b):
|
||||
return base.parser.ground, nil
|
||||
}
|
||||
|
||||
@@ -52,7 +54,7 @@ func (base baseState) Transition(s state) error {
|
||||
execBytes = append(execBytes, 0x99)
|
||||
execBytes = append(execBytes, 0x9A)
|
||||
|
||||
if sliceContains(execBytes, base.parser.context.currentChar) {
|
||||
if slices.Contains(execBytes, base.parser.context.currentChar) {
|
||||
return base.parser.execute()
|
||||
}
|
||||
}
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package ansiterm
|
||||
|
||||
func sliceContains(bytes []byte, b byte) bool {
|
||||
for _, v := range bytes {
|
||||
if v == b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package winterm
|
||||
|
||||
|
||||
+96
-90
@@ -1,14 +1,17 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package winterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//===========================================================================================================
|
||||
// ===========================================================================================================
|
||||
// IMPORTANT NOTE:
|
||||
//
|
||||
// The methods below make extensive use of the "unsafe" package to obtain the required pointers.
|
||||
@@ -26,51 +29,50 @@ import (
|
||||
// -- The value is not referenced by the method after passing the pointer to Windows
|
||||
//
|
||||
// See http://golang.org/doc/go1.3.
|
||||
//===========================================================================================================
|
||||
// ===========================================================================================================
|
||||
|
||||
var (
|
||||
kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
|
||||
kernel32DLL = windows.NewLazyDLL("kernel32.dll")
|
||||
|
||||
getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo")
|
||||
setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo")
|
||||
setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition")
|
||||
setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode")
|
||||
getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
|
||||
setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize")
|
||||
scrollConsoleScreenBufferProc = kernel32DLL.NewProc("ScrollConsoleScreenBufferA")
|
||||
setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute")
|
||||
setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo")
|
||||
writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW")
|
||||
readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW")
|
||||
waitForSingleObjectProc = kernel32DLL.NewProc("WaitForSingleObject")
|
||||
)
|
||||
|
||||
// Windows Console constants
|
||||
// Console modes
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||
const (
|
||||
// Console modes
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||
ENABLE_PROCESSED_INPUT = 0x0001
|
||||
ENABLE_LINE_INPUT = 0x0002
|
||||
ENABLE_ECHO_INPUT = 0x0004
|
||||
ENABLE_WINDOW_INPUT = 0x0008
|
||||
ENABLE_MOUSE_INPUT = 0x0010
|
||||
ENABLE_INSERT_MODE = 0x0020
|
||||
ENABLE_QUICK_EDIT_MODE = 0x0040
|
||||
ENABLE_EXTENDED_FLAGS = 0x0080
|
||||
ENABLE_AUTO_POSITION = 0x0100
|
||||
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
|
||||
ENABLE_PROCESSED_INPUT = windows.ENABLE_PROCESSED_INPUT
|
||||
ENABLE_LINE_INPUT = windows.ENABLE_LINE_INPUT
|
||||
ENABLE_ECHO_INPUT = windows.ENABLE_ECHO_INPUT
|
||||
ENABLE_WINDOW_INPUT = windows.ENABLE_WINDOW_INPUT
|
||||
ENABLE_MOUSE_INPUT = windows.ENABLE_MOUSE_INPUT
|
||||
ENABLE_INSERT_MODE = windows.ENABLE_INSERT_MODE
|
||||
ENABLE_QUICK_EDIT_MODE = windows.ENABLE_QUICK_EDIT_MODE
|
||||
ENABLE_EXTENDED_FLAGS = windows.ENABLE_EXTENDED_FLAGS
|
||||
ENABLE_AUTO_POSITION = windows.ENABLE_AUTO_POSITION
|
||||
ENABLE_VIRTUAL_TERMINAL_INPUT = windows.ENABLE_VIRTUAL_TERMINAL_INPUT
|
||||
|
||||
ENABLE_PROCESSED_OUTPUT = 0x0001
|
||||
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
|
||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
||||
DISABLE_NEWLINE_AUTO_RETURN = 0x0008
|
||||
ENABLE_LVB_GRID_WORLDWIDE = 0x0010
|
||||
ENABLE_PROCESSED_OUTPUT = windows.ENABLE_PROCESSED_OUTPUT
|
||||
ENABLE_WRAP_AT_EOL_OUTPUT = windows.ENABLE_WRAP_AT_EOL_OUTPUT
|
||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
DISABLE_NEWLINE_AUTO_RETURN = windows.DISABLE_NEWLINE_AUTO_RETURN
|
||||
ENABLE_LVB_GRID_WORLDWIDE = windows.ENABLE_LVB_GRID_WORLDWIDE
|
||||
)
|
||||
|
||||
// Character attributes
|
||||
// Note:
|
||||
// -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan).
|
||||
// Clearing all foreground or background colors results in black; setting all creates white.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes.
|
||||
// Character attributes
|
||||
//
|
||||
// Foreground and background attributes can be combined to produce colors.
|
||||
// Clearing all foreground or background color bits produces black; setting
|
||||
// all color bits produces white.
|
||||
//
|
||||
// See https://learn.microsoft.com/windows/console/char-info-str.
|
||||
const (
|
||||
FOREGROUND_BLUE uint16 = 0x0001
|
||||
FOREGROUND_GREEN uint16 = 0x0002
|
||||
FOREGROUND_RED uint16 = 0x0004
|
||||
@@ -86,23 +88,29 @@ const (
|
||||
COMMON_LVB_MASK uint16 = 0xFF00
|
||||
COMMON_LVB_REVERSE_VIDEO uint16 = 0x4000
|
||||
COMMON_LVB_UNDERSCORE uint16 = 0x8000
|
||||
)
|
||||
|
||||
// Input event types
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||
KEY_EVENT = 0x0001
|
||||
MOUSE_EVENT = 0x0002
|
||||
WINDOW_BUFFER_SIZE_EVENT = 0x0004
|
||||
MENU_EVENT = 0x0008
|
||||
FOCUS_EVENT = 0x0010
|
||||
// Input event types
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||
const (
|
||||
KEY_EVENT = windows.KEY_EVENT
|
||||
MOUSE_EVENT = windows.MOUSE_EVENT
|
||||
WINDOW_BUFFER_SIZE_EVENT = windows.WINDOW_BUFFER_SIZE_EVENT
|
||||
MENU_EVENT = windows.MENU_EVENT
|
||||
FOCUS_EVENT = windows.FOCUS_EVENT
|
||||
)
|
||||
|
||||
// WaitForSingleObject return codes
|
||||
WAIT_ABANDONED = 0x00000080
|
||||
WAIT_FAILED = 0xFFFFFFFF
|
||||
WAIT_SIGNALED = 0x0000000
|
||||
WAIT_TIMEOUT = 0x00000102
|
||||
// WaitForSingleObject return codes.
|
||||
const (
|
||||
WAIT_ABANDONED = windows.WAIT_ABANDONED
|
||||
WAIT_FAILED = windows.WAIT_FAILED
|
||||
WAIT_SIGNALED = windows.WAIT_OBJECT_0
|
||||
WAIT_TIMEOUT = 0x00000102 // Keep as untyped int for backward compat; windows.WAIT_TIMEOUT is typed as syscall.Errno.
|
||||
)
|
||||
|
||||
// WaitForSingleObject wait duration
|
||||
WAIT_INFINITE = 0xFFFFFFFF
|
||||
// WaitForSingleObject wait durations.
|
||||
const (
|
||||
WAIT_INFINITE = windows.INFINITE
|
||||
WAIT_ONE_SECOND = 1000
|
||||
WAIT_HALF_SECOND = 500
|
||||
WAIT_QUARTER_SECOND = 250
|
||||
@@ -122,6 +130,8 @@ type (
|
||||
Visible int32
|
||||
}
|
||||
|
||||
// CONSOLE_SCREEN_BUFFER_INFO is identical to [windows.ConsoleScreenBufferInfo]
|
||||
// but implements [fmt.Stringer].
|
||||
CONSOLE_SCREEN_BUFFER_INFO struct {
|
||||
Size COORD
|
||||
CursorPosition COORD
|
||||
@@ -130,17 +140,11 @@ type (
|
||||
MaximumWindowSize COORD
|
||||
}
|
||||
|
||||
COORD struct {
|
||||
X int16
|
||||
Y int16
|
||||
}
|
||||
// COORD is identical to [windows.Coord] but implements [fmt.Stringer].
|
||||
COORD windows.Coord
|
||||
|
||||
SMALL_RECT struct {
|
||||
Left int16
|
||||
Top int16
|
||||
Right int16
|
||||
Bottom int16
|
||||
}
|
||||
// SMALL_RECT is identical to [windows.SmallRect] but implements [fmt.Stringer].
|
||||
SMALL_RECT windows.SmallRect
|
||||
|
||||
// INPUT_RECORD is a C/C++ union of which KEY_EVENT_RECORD is one case, it is also the largest
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||
@@ -189,43 +193,45 @@ func SetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error
|
||||
// SetConsoleCursorPosition location of the console cursor.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx.
|
||||
func SetConsoleCursorPosition(handle uintptr, coord COORD) error {
|
||||
r1, r2, err := setConsoleCursorPositionProc.Call(handle, coordToPointer(coord))
|
||||
use(coord)
|
||||
return checkError(r1, r2, err)
|
||||
return windows.SetConsoleCursorPosition(windows.Handle(handle), windows.Coord(coord))
|
||||
}
|
||||
|
||||
// GetConsoleMode gets the console mode for given file descriptor
|
||||
// GetConsoleMode gets the console mode for the given handle.
|
||||
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx.
|
||||
func GetConsoleMode(handle uintptr) (mode uint32, err error) {
|
||||
err = syscall.GetConsoleMode(syscall.Handle(handle), &mode)
|
||||
err = windows.GetConsoleMode(windows.Handle(handle), &mode)
|
||||
return mode, err
|
||||
}
|
||||
|
||||
// SetConsoleMode sets the console mode for given file descriptor
|
||||
// SetConsoleMode sets the console mode for the given handle.
|
||||
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||
func SetConsoleMode(handle uintptr, mode uint32) error {
|
||||
r1, r2, err := setConsoleModeProc.Call(handle, uintptr(mode), 0)
|
||||
use(mode)
|
||||
return checkError(r1, r2, err)
|
||||
return windows.SetConsoleMode(windows.Handle(handle), mode)
|
||||
}
|
||||
|
||||
// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
|
||||
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx.
|
||||
func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
|
||||
info := CONSOLE_SCREEN_BUFFER_INFO{}
|
||||
err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0))
|
||||
if err != nil {
|
||||
var info windows.ConsoleScreenBufferInfo
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(handle), &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
|
||||
return &CONSOLE_SCREEN_BUFFER_INFO{
|
||||
Size: COORD(info.Size),
|
||||
CursorPosition: COORD(info.CursorPosition),
|
||||
Attributes: info.Attributes,
|
||||
Window: SMALL_RECT(info.Window),
|
||||
MaximumWindowSize: COORD(info.MaximumWindowSize),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect SMALL_RECT, destOrigin COORD, char CHAR_INFO) error {
|
||||
r1, r2, err := scrollConsoleScreenBufferProc.Call(handle, uintptr(unsafe.Pointer(&scrollRect)), uintptr(unsafe.Pointer(&clipRect)), coordToPointer(destOrigin), uintptr(unsafe.Pointer(&char)))
|
||||
use(scrollRect)
|
||||
use(clipRect)
|
||||
use(destOrigin)
|
||||
use(char)
|
||||
runtime.KeepAlive(scrollRect)
|
||||
runtime.KeepAlive(clipRect)
|
||||
runtime.KeepAlive(destOrigin)
|
||||
runtime.KeepAlive(char)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
@@ -233,7 +239,7 @@ func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect S
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686044(v=vs.85).aspx.
|
||||
func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error {
|
||||
r1, r2, err := setConsoleScreenBufferSizeProc.Call(handle, coordToPointer(coord))
|
||||
use(coord)
|
||||
runtime.KeepAlive(coord)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
@@ -242,7 +248,7 @@ func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error {
|
||||
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx.
|
||||
func SetConsoleTextAttribute(handle uintptr, attribute uint16) error {
|
||||
r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)
|
||||
use(attribute)
|
||||
runtime.KeepAlive(attribute)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
@@ -251,8 +257,8 @@ func SetConsoleTextAttribute(handle uintptr, attribute uint16) error {
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx.
|
||||
func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) error {
|
||||
r1, r2, err := setConsoleWindowInfoProc.Call(handle, uintptr(boolToBOOL(isAbsolute)), uintptr(unsafe.Pointer(&rect)))
|
||||
use(isAbsolute)
|
||||
use(rect)
|
||||
runtime.KeepAlive(isAbsolute)
|
||||
runtime.KeepAlive(rect)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
@@ -260,9 +266,9 @@ func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) erro
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx.
|
||||
func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) error {
|
||||
r1, r2, err := writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), coordToPointer(bufferSize), coordToPointer(bufferCoord), uintptr(unsafe.Pointer(writeRegion)))
|
||||
use(buffer)
|
||||
use(bufferSize)
|
||||
use(bufferCoord)
|
||||
runtime.KeepAlive(buffer)
|
||||
runtime.KeepAlive(bufferSize)
|
||||
runtime.KeepAlive(bufferCoord)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
@@ -270,7 +276,7 @@ func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bu
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx.
|
||||
func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) error {
|
||||
r1, r2, err := readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)), uintptr(unsafe.Pointer(count)))
|
||||
use(buffer)
|
||||
runtime.KeepAlive(buffer)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
@@ -278,15 +284,19 @@ func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) erro
|
||||
// It returns true if the handle was signaled; false otherwise.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx.
|
||||
func WaitForSingleObject(handle uintptr, msWait uint32) (bool, error) {
|
||||
r1, _, err := waitForSingleObjectProc.Call(handle, uintptr(uint32(msWait)))
|
||||
switch r1 {
|
||||
case WAIT_ABANDONED, WAIT_TIMEOUT:
|
||||
event, err := windows.WaitForSingleObject(windows.Handle(handle), msWait)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch event {
|
||||
case windows.WAIT_ABANDONED, uint32(windows.WAIT_TIMEOUT):
|
||||
return false, nil
|
||||
case WAIT_SIGNALED:
|
||||
case windows.WAIT_OBJECT_0:
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
use(msWait)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// String helpers
|
||||
@@ -321,7 +331,3 @@ func coordToPointer(c COORD) uintptr {
|
||||
// Note: This code assumes the two SHORTs are correctly laid out; the "cast" to uint32 is just to get a pointer to pass.
|
||||
return uintptr(*((*uint32)(unsafe.Pointer(&c))))
|
||||
}
|
||||
|
||||
// use is a no-op, but the compiler cannot see that it is.
|
||||
// Calling use(p) ensures that p is kept live until that point.
|
||||
func use(p interface{}) {}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package winterm
|
||||
|
||||
|
||||
+11
-33
@@ -1,10 +1,12 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package winterm
|
||||
|
||||
const (
|
||||
horizontal = iota
|
||||
vertical
|
||||
line
|
||||
column
|
||||
)
|
||||
|
||||
func (h *windowsAnsiEventHandler) getCursorWindow(info *CONSOLE_SCREEN_BUFFER_INFO) SMALL_RECT {
|
||||
@@ -58,44 +60,20 @@ func (h *windowsAnsiEventHandler) moveCursor(moveMode int, param int) error {
|
||||
position.X += int16(param)
|
||||
case vertical:
|
||||
position.Y += int16(param)
|
||||
case line:
|
||||
position.X = 0
|
||||
position.Y += int16(param)
|
||||
case column:
|
||||
position.X = int16(param) - 1
|
||||
}
|
||||
|
||||
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return h.setCursorPosition(position, h.getCursorWindow(info))
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) moveCursorLine(param int) error {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
position := info.CursorPosition
|
||||
position.X = 0
|
||||
position.Y += int16(param)
|
||||
|
||||
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return h.moveCursor(line, param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) moveCursorColumn(param int) error {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
position := info.CursorPosition
|
||||
position.X = int16(param) - 1
|
||||
|
||||
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return h.moveCursor(column, param)
|
||||
}
|
||||
|
||||
+3
-13
@@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package winterm
|
||||
|
||||
@@ -50,12 +50,7 @@ func (h *windowsAnsiEventHandler) clearRange(attributes uint16, fromCoord COORD,
|
||||
coordStart.X, coordStart.Y = xCurrent, yCurrent
|
||||
coordEnd.X, coordEnd.Y = xEnd, yEnd
|
||||
|
||||
err = h.clearRect(attributes, coordStart, coordEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return h.clearRect(attributes, coordStart, coordEnd)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) clearRect(attributes uint16, fromCoord COORD, toCoord COORD) error {
|
||||
@@ -75,10 +70,5 @@ func (h *windowsAnsiEventHandler) clearRect(attributes uint16, fromCoord COORD,
|
||||
buffer[i] = char
|
||||
}
|
||||
|
||||
err := WriteConsoleOutput(h.fd, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, ®ion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return WriteConsoleOutput(h.fd, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, ®ion)
|
||||
}
|
||||
|
||||
+4
-11
@@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package winterm
|
||||
|
||||
@@ -39,9 +39,8 @@ func (h *windowsAnsiEventHandler) deleteLines(param int) error {
|
||||
if start >= sr.top && start <= sr.bottom {
|
||||
sr.top = start
|
||||
return h.scroll(param, sr, info)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) insertLines(param int) error {
|
||||
@@ -72,10 +71,7 @@ func (h *windowsAnsiEventHandler) scroll(param int, sr scrollRegion, info *CONSO
|
||||
Attributes: h.attributes,
|
||||
}
|
||||
|
||||
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) deleteCharacters(param int) error {
|
||||
@@ -111,8 +107,5 @@ func (h *windowsAnsiEventHandler) scrollLine(columns int, position COORD, info *
|
||||
Attributes: h.attributes,
|
||||
}
|
||||
|
||||
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package winterm
|
||||
|
||||
|
||||
+19
-33
@@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package winterm
|
||||
|
||||
@@ -169,9 +169,7 @@ func (h *windowsAnsiEventHandler) executeLF() error {
|
||||
return err
|
||||
}
|
||||
h.logf("Resetting cursor position for LF without CR")
|
||||
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||
return err
|
||||
}
|
||||
return SetConsoleCursorPosition(h.fd, pos)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -373,7 +371,7 @@ func (h *windowsAnsiEventHandler) CUP(row int, col int) error {
|
||||
}
|
||||
|
||||
window := h.getCursorWindow(info)
|
||||
position := COORD{window.Left + int16(col) - 1, window.Top + int16(row) - 1}
|
||||
position := COORD{X: window.Left + int16(col) - 1, Y: window.Top + int16(row) - 1}
|
||||
return h.setCursorPosition(position, window)
|
||||
}
|
||||
|
||||
@@ -423,7 +421,7 @@ func (h *windowsAnsiEventHandler) DECCOLM(use132 bool) error {
|
||||
targetWidth = 132
|
||||
}
|
||||
if info.Size.X < targetWidth {
|
||||
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
|
||||
if err := SetConsoleScreenBufferSize(h.fd, COORD{X: targetWidth, Y: info.Size.Y}); err != nil {
|
||||
h.logf("set buffer failed: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -436,12 +434,12 @@ func (h *windowsAnsiEventHandler) DECCOLM(use132 bool) error {
|
||||
return err
|
||||
}
|
||||
if info.Size.X > targetWidth {
|
||||
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
|
||||
if err := SetConsoleScreenBufferSize(h.fd, COORD{X: targetWidth, Y: info.Size.Y}); err != nil {
|
||||
h.logf("set buffer failed: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return SetConsoleCursorPosition(h.fd, COORD{0, 0})
|
||||
return SetConsoleCursorPosition(h.fd, COORD{X: 0, Y: 0})
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) ED(param int) error {
|
||||
@@ -468,15 +466,15 @@ func (h *windowsAnsiEventHandler) ED(param int) error {
|
||||
switch param {
|
||||
case 0:
|
||||
start = info.CursorPosition
|
||||
end = COORD{info.Size.X - 1, info.Size.Y - 1}
|
||||
end = COORD{X: info.Size.X - 1, Y: info.Size.Y - 1}
|
||||
|
||||
case 1:
|
||||
start = COORD{0, 0}
|
||||
start = COORD{X: 0, Y: 0}
|
||||
end = info.CursorPosition
|
||||
|
||||
case 2:
|
||||
start = COORD{0, 0}
|
||||
end = COORD{info.Size.X - 1, info.Size.Y - 1}
|
||||
start = COORD{X: 0, Y: 0}
|
||||
end = COORD{X: info.Size.X - 1, Y: info.Size.Y - 1}
|
||||
}
|
||||
|
||||
err = h.clearRange(h.attributes, start, end)
|
||||
@@ -495,9 +493,7 @@ func (h *windowsAnsiEventHandler) ED(param int) error {
|
||||
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
|
||||
return err
|
||||
}
|
||||
return SetConsoleWindowInfo(h.fd, true, window)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -525,23 +521,18 @@ func (h *windowsAnsiEventHandler) EL(param int) error {
|
||||
switch param {
|
||||
case 0:
|
||||
start = info.CursorPosition
|
||||
end = COORD{info.Size.X, info.CursorPosition.Y}
|
||||
end = COORD{X: info.Size.X, Y: info.CursorPosition.Y}
|
||||
|
||||
case 1:
|
||||
start = COORD{0, info.CursorPosition.Y}
|
||||
start = COORD{X: 0, Y: info.CursorPosition.Y}
|
||||
end = info.CursorPosition
|
||||
|
||||
case 2:
|
||||
start = COORD{0, info.CursorPosition.Y}
|
||||
end = COORD{info.Size.X, info.CursorPosition.Y}
|
||||
start = COORD{X: 0, Y: info.CursorPosition.Y}
|
||||
end = COORD{X: info.Size.X, Y: info.CursorPosition.Y}
|
||||
}
|
||||
|
||||
err = h.clearRange(h.attributes, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return h.clearRange(h.attributes, start, end)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) IL(param int) error {
|
||||
@@ -611,12 +602,7 @@ func (h *windowsAnsiEventHandler) SGR(params []int) error {
|
||||
if h.inverted {
|
||||
attributes = invertAttributes(attributes)
|
||||
}
|
||||
err := SetConsoleTextAttribute(h.fd, attributes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return SetConsoleTextAttribute(h.fd, attributes)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) SU(param int) error {
|
||||
@@ -702,8 +688,8 @@ func (h *windowsAnsiEventHandler) Flush() error {
|
||||
}
|
||||
|
||||
charInfo := []CHAR_INFO{{UnicodeChar: uint16(h.marginByte), Attributes: info.Attributes}}
|
||||
size := COORD{1, 1}
|
||||
position := COORD{0, 0}
|
||||
size := COORD{X: 1, Y: 1}
|
||||
position := COORD{X: 0, Y: 0}
|
||||
region := SMALL_RECT{Left: info.CursorPosition.X, Top: info.CursorPosition.Y, Right: info.CursorPosition.X, Bottom: info.CursorPosition.Y}
|
||||
if err := WriteConsoleOutput(h.fd, charInfo, size, position, ®ion); err != nil {
|
||||
return err
|
||||
|
||||
+25
-7
@@ -3,8 +3,9 @@
|
||||
package winio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -20,16 +21,33 @@ import (
|
||||
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
||||
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
||||
|
||||
var (
|
||||
ErrFileClosed = errors.New("file has already been closed")
|
||||
ErrTimeout = &timeoutError{}
|
||||
)
|
||||
// ErrFileClosed is returned when an operation is performed on a closed file.
|
||||
//
|
||||
// Deprecated: use [os.ErrClosed] instead.
|
||||
var ErrFileClosed = os.ErrClosed
|
||||
|
||||
// ErrTimeout is returned when an I/O operation exceeds its deadline.
|
||||
//
|
||||
// It matches both [context.DeadlineExceeded] and [os.ErrDeadlineExceeded]
|
||||
// with [errors.Is].
|
||||
var ErrTimeout error = &timeoutError{}
|
||||
|
||||
// timeoutError preserves the historical "i/o timeout" error while matching
|
||||
// both context.DeadlineExceeded and os.ErrDeadlineExceeded through errors.Is.
|
||||
// This mirrors the behavior of net.errTimeout:
|
||||
// https://github.com/golang/go/blob/go1.27.1/src/net/net.go
|
||||
//
|
||||
// TODO: consider replacing ErrTimeout with os.ErrDeadlineExceeded if os.ErrDeadlineExceeded is changed to match context.DeadlineExceeded.
|
||||
type timeoutError struct{}
|
||||
|
||||
func (*timeoutError) Error() string { return "i/o timeout" }
|
||||
func (*timeoutError) Timeout() bool { return true }
|
||||
func (*timeoutError) Temporary() bool { return true }
|
||||
func (*timeoutError) Unwrap() error { return os.ErrDeadlineExceeded }
|
||||
|
||||
func (*timeoutError) Is(err error) bool {
|
||||
return err == context.DeadlineExceeded
|
||||
}
|
||||
|
||||
type timeoutChan chan struct{}
|
||||
|
||||
@@ -143,7 +161,7 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
f.wgLock.RLock()
|
||||
if f.closing.Load() {
|
||||
f.wgLock.RUnlock()
|
||||
return nil, ErrFileClosed
|
||||
return nil, os.ErrClosed
|
||||
}
|
||||
f.wg.Add(1)
|
||||
f.wgLock.RUnlock()
|
||||
@@ -192,7 +210,7 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
|
||||
err = r.err
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if f.closing.Load() {
|
||||
err = ErrFileClosed
|
||||
err = os.ErrClosed
|
||||
}
|
||||
} else if err != nil && f.socket {
|
||||
// err is from Win32. Query the overlapped structure to get the winsock error.
|
||||
|
||||
+1
-1
@@ -441,7 +441,7 @@ func canRedial(err error) bool {
|
||||
|
||||
func (conn *HvsockConn) opErr(op string, err error) error {
|
||||
// translate from "file closed" to "socket closed"
|
||||
if errors.Is(err, ErrFileClosed) {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
err = socket.ErrSocketClosed
|
||||
}
|
||||
return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err}
|
||||
|
||||
+11
-12
@@ -102,12 +102,10 @@ func (status ntStatus) Err() error {
|
||||
return rtlNtStatusToDosError(status)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
ErrPipeListenerClosed = net.ErrClosed
|
||||
|
||||
errPipeWriteClosed = errors.New("pipe has been closed for write")
|
||||
)
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
//
|
||||
// Deprecated: use [net.ErrClosed] instead.
|
||||
var ErrPipeListenerClosed = net.ErrClosed
|
||||
|
||||
type win32Pipe struct {
|
||||
*win32File
|
||||
@@ -144,9 +142,10 @@ func (f *win32Pipe) Disconnect() error {
|
||||
}
|
||||
|
||||
// CloseWrite closes the write side of a message pipe in byte mode.
|
||||
// It returns [io.ErrClosedPipe] if the write side is already closed.
|
||||
func (f *win32MessageBytePipe) CloseWrite() error {
|
||||
if f.writeClosed {
|
||||
return errPipeWriteClosed
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
err := f.win32File.Flush()
|
||||
if err != nil {
|
||||
@@ -161,10 +160,10 @@ func (f *win32MessageBytePipe) CloseWrite() error {
|
||||
}
|
||||
|
||||
// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
|
||||
// they are used to implement CloseWrite().
|
||||
// they are used to implement CloseWrite. It returns [io.ErrClosedPipe] if the write side is closed.
|
||||
func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
|
||||
if f.writeClosed {
|
||||
return 0, errPipeWriteClosed
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
@@ -454,7 +453,7 @@ func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
|
||||
_ = p.Close()
|
||||
p = nil
|
||||
<-ch
|
||||
err = ErrPipeListenerClosed
|
||||
err = net.ErrClosed
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
@@ -479,7 +478,7 @@ func (l *win32PipeListener) listenerRoutine() {
|
||||
}
|
||||
}
|
||||
responseCh <- acceptResponse{p, err}
|
||||
closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
|
||||
closed = err == net.ErrClosed //nolint:errorlint // err is Errno
|
||||
}
|
||||
}
|
||||
windows.Close(l.firstHandle)
|
||||
@@ -571,7 +570,7 @@ func (l *win32PipeListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
return &win32Pipe{win32File: response.f, path: l.path}, nil
|
||||
case <-l.doneCh:
|
||||
return nil, ErrPipeListenerClosed
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+33
-3
@@ -408,7 +408,10 @@ func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWe
|
||||
(*obj.protected)[k] = makeRawMessage(b)
|
||||
}
|
||||
|
||||
authData := obj.computeAuthData()
|
||||
authData, err := obj.computeAuthData()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts, err := ctx.cipher.encrypt(cek, authData, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -428,6 +431,13 @@ func (ctx *genericEncrypter) Options() EncrypterOptions {
|
||||
}
|
||||
}
|
||||
|
||||
func validateContentEncryptionKeySize(cek []byte, cipher contentCipher) error {
|
||||
if len(cek) != cipher.keySize() {
|
||||
return ErrInvalidKeySize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decrypt and validate the object and return the plaintext. This
|
||||
// function does not support multi-recipient. If you desire multi-recipient
|
||||
// decryption use DecryptMulti instead.
|
||||
@@ -485,7 +495,10 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
|
||||
tag: obj.tag,
|
||||
}
|
||||
|
||||
authData := obj.computeAuthData()
|
||||
authData, err := obj.computeAuthData()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plaintext []byte
|
||||
recipient := obj.recipients[0]
|
||||
@@ -495,6 +508,10 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
|
||||
if err != nil {
|
||||
return nil, ErrCryptoFailure
|
||||
}
|
||||
err = validateContentEncryptionKeySize(cek, cipher)
|
||||
if err != nil {
|
||||
return nil, ErrCryptoFailure
|
||||
}
|
||||
|
||||
// Found a valid CEK -- let's try to decrypt.
|
||||
plaintext, err = cipher.decrypt(cek, authData, parts)
|
||||
@@ -520,6 +537,12 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
|
||||
//
|
||||
// Automatically decompresses plaintext, but returns an error if the decompressed
|
||||
// data would be >250kB or >3x the size of the compressed data, whichever is larger.
|
||||
//
|
||||
// Calling DecryptMulti() with PBES2 enabled can result in excessive CPU work.
|
||||
// The PBES2 key unwrapping algorithms are intentionally expensive, and have to
|
||||
// be run for each recipient. To be safe: don't allow PBES2 algorithms in calls
|
||||
// to ParseEncrypted / ParseEncryptedJSON. If your application requires PBES2,
|
||||
// prefer Decrypt(), which will only process single-recipient messages.
|
||||
func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) {
|
||||
globalHeaders := obj.mergedHeaders(nil)
|
||||
|
||||
@@ -553,7 +576,10 @@ func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Heade
|
||||
tag: obj.tag,
|
||||
}
|
||||
|
||||
authData := obj.computeAuthData()
|
||||
authData, err := obj.computeAuthData()
|
||||
if err != nil {
|
||||
return -1, Header{}, nil, err
|
||||
}
|
||||
|
||||
index := -1
|
||||
var plaintext []byte
|
||||
@@ -572,6 +598,10 @@ func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Heade
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
err = validateContentEncryptionKeySize(cek, cipher)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found a valid CEK -- let's try to decrypt.
|
||||
plaintext, err = cipher.decrypt(cek, authData, parts)
|
||||
|
||||
+6
-32
@@ -30,30 +30,6 @@ import (
|
||||
"github.com/go-jose/go-jose/v4/json"
|
||||
)
|
||||
|
||||
// Helper function to serialize known-good objects.
|
||||
// Precondition: value is not a nil pointer.
|
||||
func mustSerializeJSON(value interface{}) []byte {
|
||||
out, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// We never want to serialize the top-level value "null," since it's not a
|
||||
// valid JOSE message. But if a caller passes in a nil pointer to this method,
|
||||
// MarshalJSON will happily serialize it as the top-level value "null". If
|
||||
// that value is then embedded in another operation, for instance by being
|
||||
// base64-encoded and fed as input to a signing algorithm
|
||||
// (https://github.com/go-jose/go-jose/issues/22), the result will be
|
||||
// incorrect. Because this method is intended for known-good objects, and a nil
|
||||
// pointer is not a known-good object, we are free to panic in this case.
|
||||
// Note: It's not possible to directly check whether the data pointed at by an
|
||||
// interface is a nil pointer, so we do this hacky workaround.
|
||||
// https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I
|
||||
if string(out) == "null" {
|
||||
panic("Tried to serialize a nil pointer.")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Strip all newlines and whitespace
|
||||
func stripWhitespace(data string) string {
|
||||
buf := strings.Builder{}
|
||||
@@ -122,6 +98,8 @@ func inflate(input []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// byteBuffer represents a slice of bytes that can be serialized to url-safe base64.
|
||||
//
|
||||
// A nil byteBuffer represents an empty slice of bytes.
|
||||
type byteBuffer struct {
|
||||
data []byte
|
||||
}
|
||||
@@ -169,13 +147,13 @@ func (b *byteBuffer) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
*b = *newBuffer(decoded)
|
||||
b.data = decoded
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *byteBuffer) base64() string {
|
||||
return base64.RawURLEncoding.EncodeToString(b.data)
|
||||
return base64.RawURLEncoding.EncodeToString(b.bytes())
|
||||
}
|
||||
|
||||
func (b *byteBuffer) bytes() []byte {
|
||||
@@ -186,12 +164,8 @@ func (b *byteBuffer) bytes() []byte {
|
||||
return b.data
|
||||
}
|
||||
|
||||
func (b byteBuffer) bigInt() *big.Int {
|
||||
return new(big.Int).SetBytes(b.data)
|
||||
}
|
||||
|
||||
func (b byteBuffer) toInt() int {
|
||||
return int(b.bigInt().Int64())
|
||||
func (b *byteBuffer) bigInt() *big.Int {
|
||||
return new(big.Int).SetBytes(b.bytes())
|
||||
}
|
||||
|
||||
func base64EncodeLen(sl []byte) int {
|
||||
|
||||
+13
@@ -671,6 +671,19 @@ func (d *decodeState) object(v reflect.Value) {
|
||||
for _, i := range f.index {
|
||||
if subv.Kind() == reflect.Ptr {
|
||||
if subv.IsNil() {
|
||||
// If a struct embeds a pointer to an unexported type,
|
||||
// it is not possible to set a newly allocated value
|
||||
// since the field is unexported.
|
||||
//
|
||||
// See https://golang.org/issue/21357
|
||||
if !subv.CanSet() {
|
||||
d.saveError(fmt.Errorf("json: cannot set embedded pointer to unexported struct: %v", subv.Type().Elem()))
|
||||
// Invalidate subv to ensure d.value(subv) skips over
|
||||
// the JSON value without assigning it to subv.
|
||||
subv = reflect.Value{}
|
||||
destring = false
|
||||
break
|
||||
}
|
||||
subv.Set(reflect.New(subv.Type().Elem()))
|
||||
}
|
||||
subv = subv.Elem()
|
||||
|
||||
+13
-1
@@ -1025,7 +1025,19 @@ func typeFields(t reflect.Type) []field {
|
||||
// Scan f.typ for fields to include.
|
||||
for i := 0; i < f.typ.NumField(); i++ {
|
||||
sf := f.typ.Field(i)
|
||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||
if sf.Anonymous {
|
||||
t := sf.Type
|
||||
if t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
if !sf.IsExported() && t.Kind() != reflect.Struct {
|
||||
// Ignore embedded fields of unexported non-struct types.
|
||||
continue
|
||||
}
|
||||
// Do not ignore embedded fields of unexported struct types
|
||||
// since they may have exported fields.
|
||||
} else if !sf.IsExported() {
|
||||
// Ignore unexported non-embedded fields.
|
||||
continue
|
||||
}
|
||||
tag := sf.Tag.Get("json")
|
||||
|
||||
+59
-25
@@ -25,6 +25,12 @@ import (
|
||||
"github.com/go-jose/go-jose/v4/json"
|
||||
)
|
||||
|
||||
// MaxRecipients is the maximum number of recipients of a JWE at parse time.
|
||||
// Higher values mean more potential work done across untrusted input.
|
||||
//
|
||||
// If modifying MaxRecipients, do it before any parsing occurs.
|
||||
var MaxRecipients = 32
|
||||
|
||||
// rawJSONWebEncryption represents a raw JWE JSON object. Used for parsing/serializing.
|
||||
type rawJSONWebEncryption struct {
|
||||
Protected *byteBuffer `json:"protected,omitempty"`
|
||||
@@ -84,14 +90,18 @@ func (obj JSONWebEncryption) mergedHeaders(recipient *recipientInfo) rawHeader {
|
||||
}
|
||||
|
||||
// Get the additional authenticated data from a JWE object.
|
||||
func (obj JSONWebEncryption) computeAuthData() []byte {
|
||||
func (obj JSONWebEncryption) computeAuthData() ([]byte, error) {
|
||||
var protected string
|
||||
|
||||
switch {
|
||||
case obj.original != nil && obj.original.Protected != nil:
|
||||
protected = obj.original.Protected.base64()
|
||||
case obj.protected != nil:
|
||||
protected = base64.RawURLEncoding.EncodeToString(mustSerializeJSON((obj.protected)))
|
||||
serializedProtected, err := json.Marshal(obj.protected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
protected = base64.RawURLEncoding.EncodeToString(serializedProtected)
|
||||
default:
|
||||
protected = ""
|
||||
}
|
||||
@@ -102,7 +112,7 @@ func (obj JSONWebEncryption) computeAuthData() []byte {
|
||||
output = append(output, []byte(base64.RawURLEncoding.EncodeToString(obj.aad))...)
|
||||
}
|
||||
|
||||
return output
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func containsKeyAlgorithm(haystack []KeyAlgorithm, needle KeyAlgorithm) bool {
|
||||
@@ -128,11 +138,10 @@ func containsContentEncryption(haystack []ContentEncryption, needle ContentEncry
|
||||
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.1
|
||||
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.2
|
||||
//
|
||||
// The keyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
|
||||
// header parameters respectively. They must be nonempty, and each "alg" or "enc" header in
|
||||
// parsed data must contain a value that is present in the corresponding parameter. That
|
||||
// includes the protected and unprotected headers as well as all recipients. To accept
|
||||
// multiple algorithms, pass a slice of all the algorithms you want to accept.
|
||||
// The keyAlgorithms and contentEncryption parameters contain the allowable algorithms in the "alg" and "enc"
|
||||
// header parameters respectively. They must be non-empty. Non-matching algorithms cause a parse error.
|
||||
//
|
||||
// Deprecated: use ParseEncryptedJSON or ParseEncryptedCompact depending on which serialization you expect.
|
||||
func ParseEncrypted(input string,
|
||||
keyEncryptionAlgorithms []KeyAlgorithm,
|
||||
contentEncryption []ContentEncryption,
|
||||
@@ -145,9 +154,13 @@ func ParseEncrypted(input string,
|
||||
return ParseEncryptedCompact(input, keyEncryptionAlgorithms, contentEncryption)
|
||||
}
|
||||
|
||||
// ParseEncryptedJSON parses a message in JWE JSON Serialization.
|
||||
// ParseEncryptedJSON parses a message in JWE JSON Serialization. Messages with
|
||||
// more than [MaxRecipients] recipients are rejected.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.2
|
||||
//
|
||||
// The keyAlgorithms and contentEncryption parameters contain the allowable algorithms in the "alg" and "enc"
|
||||
// header parameters respectively. They must be non-empty. Any non-allowed algorithms will cause parsing to fail.
|
||||
func ParseEncryptedJSON(
|
||||
input string,
|
||||
keyEncryptionAlgorithms []KeyAlgorithm,
|
||||
@@ -177,6 +190,9 @@ func (parsed *rawJSONWebEncryption) sanitized(
|
||||
if len(contentEncryption) == 0 {
|
||||
return nil, errors.New("go-jose/go-jose: no content encryption algorithms provided")
|
||||
}
|
||||
if len(parsed.Recipients) > MaxRecipients {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: too many recipients in JWE; maximum is %d", MaxRecipients)
|
||||
}
|
||||
|
||||
obj := &JSONWebEncryption{
|
||||
original: parsed,
|
||||
@@ -195,21 +211,11 @@ func (parsed *rawJSONWebEncryption) sanitized(
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.Protected != nil && len(parsed.Protected.bytes()) > 0 {
|
||||
err := json.Unmarshal(parsed.Protected.bytes(), &obj.protected)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid protected header: %s, %s", err, parsed.Protected.base64())
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this must be called _after_ we parse the protected header,
|
||||
// otherwise fields from the protected header will not get picked up.
|
||||
var err error
|
||||
mergedHeaders := obj.mergedHeaders(nil)
|
||||
obj.Header, err = mergedHeaders.sanitized()
|
||||
protected, err := parseProtectedHeader(parsed.Protected)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: cannot sanitize merged headers: %v (%v)", err, mergedHeaders)
|
||||
return nil, err
|
||||
}
|
||||
obj.protected = protected
|
||||
|
||||
if len(parsed.Recipients) == 0 {
|
||||
obj.recipients = []recipientInfo{
|
||||
@@ -264,6 +270,14 @@ func (parsed *rawJSONWebEncryption) sanitized(
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this must be called _after_ we parse the protected header,
|
||||
// otherwise fields from the protected header will not get picked up.
|
||||
mergedHeaders := obj.mergedHeaders(nil)
|
||||
obj.Header, err = mergedHeaders.sanitized()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: cannot sanitize merged headers: %w", err)
|
||||
}
|
||||
|
||||
obj.iv = parsed.Iv.bytes()
|
||||
obj.ciphertext = parsed.Ciphertext.bytes()
|
||||
obj.tag = parsed.Tag.bytes()
|
||||
@@ -287,6 +301,9 @@ func validateAlgEnc(headers rawHeader, keyAlgorithms []KeyAlgorithm, contentEncr
|
||||
// ParseEncryptedCompact parses a message in JWE Compact Serialization.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.1
|
||||
//
|
||||
// The keyAlgorithms and contentEncryption parameters contain the allowable algorithms in the "alg" and "enc"
|
||||
// header parameters respectively. They must be non-empty. Any non-allowed algorithms will cause parsing to fail.
|
||||
func ParseEncryptedCompact(
|
||||
input string,
|
||||
keyAlgorithms []KeyAlgorithm,
|
||||
@@ -354,7 +371,10 @@ func (obj JSONWebEncryption) CompactSerialize() (string, error) {
|
||||
return "", ErrNotSupported
|
||||
}
|
||||
|
||||
serializedProtected := mustSerializeJSON(obj.protected)
|
||||
serializedProtected, err := json.Marshal(obj.protected)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64JoinWithDots(
|
||||
serializedProtected,
|
||||
@@ -366,7 +386,13 @@ func (obj JSONWebEncryption) CompactSerialize() (string, error) {
|
||||
}
|
||||
|
||||
// FullSerialize serializes an object using the full JSON serialization format.
|
||||
//
|
||||
// It returns "" if obj cannot be serialized, for example if it has no recipients.
|
||||
func (obj JSONWebEncryption) FullSerialize() string {
|
||||
if len(obj.recipients) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
raw := rawJSONWebEncryption{
|
||||
Unprotected: obj.unprotected,
|
||||
Iv: newBuffer(obj.iv),
|
||||
@@ -392,8 +418,16 @@ func (obj JSONWebEncryption) FullSerialize() string {
|
||||
}
|
||||
|
||||
if obj.protected != nil {
|
||||
raw.Protected = newBuffer(mustSerializeJSON(obj.protected))
|
||||
serializedProtected, err := json.Marshal(obj.protected)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
raw.Protected = newBuffer(serializedProtected)
|
||||
}
|
||||
|
||||
return string(mustSerializeJSON(raw))
|
||||
out, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
+74
-24
@@ -30,6 +30,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"reflect"
|
||||
@@ -177,13 +178,29 @@ func (k JSONWebKey) MarshalJSON() ([]byte, error) {
|
||||
// UnmarshalJSON reads a key from its JSON representation.
|
||||
//
|
||||
// Returns ErrUnsupportedKeyType for unrecognized or unsupported "kty" header values.
|
||||
func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
|
||||
//
|
||||
// Directly unmarshaling a JWK is not safe for untrusted input.
|
||||
func (k *JSONWebKey) UnmarshalJSON(data []byte) error {
|
||||
return k.unmarshalJSON(data, true)
|
||||
}
|
||||
|
||||
// unmarshalJSON reads a key from its JSON representation.
|
||||
//
|
||||
// If allowPrivate is false, a JWK with private key material is rejected and the
|
||||
// function is safe for use against untrusted input.
|
||||
func (k *JSONWebKey) unmarshalJSON(data []byte, allowPrivate bool) (err error) {
|
||||
var raw rawJSONWebKey
|
||||
err = json.Unmarshal(data, &raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !allowPrivate &&
|
||||
(raw.K != nil || raw.D != nil || raw.P != nil || raw.Q != nil ||
|
||||
raw.Dp != nil || raw.Dq != nil || raw.Qi != nil) {
|
||||
return errors.New("go-jose/go-jose: JWK contains private parameters")
|
||||
}
|
||||
|
||||
certs, err := parseCertificateChain(raw.X5c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("go-jose/go-jose: failed to unmarshal x5c field: %s", err)
|
||||
@@ -233,7 +250,7 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
|
||||
keyPub = key
|
||||
}
|
||||
case "oct":
|
||||
if certPub != nil {
|
||||
if len(certs) > 0 {
|
||||
return errors.New("go-jose/go-jose: invalid JWK, found 'oct' (symmetric) key with cert chain")
|
||||
}
|
||||
key, err = raw.symmetricKey()
|
||||
@@ -272,10 +289,8 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
|
||||
return ErrUnsupportedKeyType
|
||||
}
|
||||
|
||||
if certPub != nil && keyPub != nil {
|
||||
if !reflect.DeepEqual(certPub, keyPub) {
|
||||
return errors.New("go-jose/go-jose: invalid JWK, public keys in key and x5c fields do not match")
|
||||
}
|
||||
if len(certs) > 0 && (certPub == nil || !reflect.DeepEqual(certPub, keyPub)) {
|
||||
return errors.New("go-jose/go-jose: invalid JWK, public keys in key and x5c fields do not match")
|
||||
}
|
||||
|
||||
*k = JSONWebKey{Key: key, KeyID: raw.Kid, Algorithm: raw.Alg, Use: raw.Use, Certificates: certs}
|
||||
@@ -392,6 +407,9 @@ func ecThumbprintInput(curve elliptic.Curve, x, y *big.Int) (string, error) {
|
||||
}
|
||||
|
||||
func rsaThumbprintInput(n *big.Int, e int) (string, error) {
|
||||
if n == nil || e <= 0 {
|
||||
return "", errors.New("go-jose/go-jose: invalid RSA key")
|
||||
}
|
||||
return fmt.Sprintf(rsaThumbprintTemplate,
|
||||
newBufferFromInt(uint64(e)).base64(),
|
||||
newBuffer(n.Bytes()).base64()), nil
|
||||
@@ -505,14 +523,41 @@ func (k *JSONWebKey) Valid() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MaxRSAModulusBits is the largest RSA modulus size, in bits, that will be
|
||||
// accepted when parsing a JWK or verifying an x5c certificate chain with
|
||||
// [Header.Certificates]. Applications that need larger keys can raise this
|
||||
// limit.
|
||||
//
|
||||
// If modifying MaxRSAModulusBits, do it before any parsing occurs.
|
||||
var MaxRSAModulusBits = 4096
|
||||
|
||||
func (key rawJSONWebKey) rsaPublicKey() (*rsa.PublicKey, error) {
|
||||
if key.N == nil || key.E == nil {
|
||||
return rsaPublicKey(key.N, key.E)
|
||||
}
|
||||
|
||||
func rsaPublicKey(N, E *byteBuffer) (*rsa.PublicKey, error) {
|
||||
if N == nil || E == nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, missing n/e values")
|
||||
}
|
||||
|
||||
n := N.bigInt()
|
||||
if n.BitLen() > MaxRSAModulusBits {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, modulus too large")
|
||||
}
|
||||
|
||||
e := new(big.Int).SetBytes(E.bytes())
|
||||
if !e.IsInt64() {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, bad e value")
|
||||
}
|
||||
|
||||
eInt64 := e.Int64()
|
||||
if eInt64 < 2 || eInt64 > math.MaxInt {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key, bad e value")
|
||||
}
|
||||
|
||||
return &rsa.PublicKey{
|
||||
N: key.N.bigInt(),
|
||||
E: key.E.toInt(),
|
||||
N: n,
|
||||
E: int(eInt64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -556,11 +601,11 @@ func (key rawJSONWebKey) ecPublicKey() (*ecdsa.PublicKey, error) {
|
||||
// The length of this octet string MUST be the full size of a coordinate for
|
||||
// the curve specified in the "crv" parameter.
|
||||
// https://tools.ietf.org/html/rfc7518#section-6.2.1.2
|
||||
if curveSize(curve) != len(key.X.data) {
|
||||
if curveSize(curve) != len(key.X.bytes()) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC public key, wrong length for x")
|
||||
}
|
||||
|
||||
if curveSize(curve) != len(key.Y.data) {
|
||||
if curveSize(curve) != len(key.Y.bytes()) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC public key, wrong length for y")
|
||||
}
|
||||
|
||||
@@ -673,16 +718,21 @@ func (key rawJSONWebKey) rsaPrivateKey() (*rsa.PrivateKey, error) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA private key, missing %s value(s)", strings.Join(missing, ", "))
|
||||
}
|
||||
|
||||
pubKey, err := rsaPublicKey(key.N, key.E)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := pubKey.N
|
||||
p, q := key.P.bigInt(), key.Q.bigInt()
|
||||
if p.BitLen()+q.BitLen() > n.BitLen()+1 || new(big.Int).Mul(p, q).Cmp(n) != 0 {
|
||||
return nil, errors.New("go-jose/go-jose: invalid RSA private key, p * q != n")
|
||||
}
|
||||
|
||||
rv := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{
|
||||
N: key.N.bigInt(),
|
||||
E: key.E.toInt(),
|
||||
},
|
||||
D: key.D.bigInt(),
|
||||
Primes: []*big.Int{
|
||||
key.P.bigInt(),
|
||||
key.Q.bigInt(),
|
||||
},
|
||||
PublicKey: *pubKey,
|
||||
D: key.D.bigInt(),
|
||||
Primes: []*big.Int{p, q},
|
||||
}
|
||||
|
||||
if key.Dp != nil {
|
||||
@@ -695,7 +745,7 @@ func (key rawJSONWebKey) rsaPrivateKey() (*rsa.PrivateKey, error) {
|
||||
rv.Precomputed.Qinv = key.Qi.bigInt()
|
||||
}
|
||||
|
||||
err := rv.Validate()
|
||||
err = rv.Validate()
|
||||
return rv, err
|
||||
}
|
||||
|
||||
@@ -769,16 +819,16 @@ func (key rawJSONWebKey) ecPrivateKey() (*ecdsa.PrivateKey, error) {
|
||||
// The length of this octet string MUST be the full size of a coordinate for
|
||||
// the curve specified in the "crv" parameter.
|
||||
// https://tools.ietf.org/html/rfc7518#section-6.2.1.2
|
||||
if curveSize(curve) != len(key.X.data) {
|
||||
if curveSize(curve) != len(key.X.bytes()) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for x")
|
||||
}
|
||||
|
||||
if curveSize(curve) != len(key.Y.data) {
|
||||
if curveSize(curve) != len(key.Y.bytes()) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for y")
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7518#section-6.2.2.1
|
||||
if dSize(curve) != len(key.D.data) {
|
||||
if dSize(curve) != len(key.D.bytes()) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for d")
|
||||
}
|
||||
|
||||
|
||||
+79
-32
@@ -27,6 +27,12 @@ import (
|
||||
"github.com/go-jose/go-jose/v4/json"
|
||||
)
|
||||
|
||||
// MaxSignatures is the maximum number of signatures in a JWS at parse time.
|
||||
// Higher values mean more potential work done across untrusted input.
|
||||
//
|
||||
// If modifying MaxSignatures, do it before any parsing occurs.
|
||||
var MaxSignatures = 32
|
||||
|
||||
// rawJSONWebSignature represents a raw JWS JSON object. Used for parsing/serializing.
|
||||
type rawJSONWebSignature struct {
|
||||
Payload *byteBuffer `json:"payload,omitempty"`
|
||||
@@ -86,6 +92,11 @@ type Signature struct {
|
||||
// Verify() after parsing to validate the signature and obtain the payload.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7515#section-7
|
||||
//
|
||||
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
|
||||
// It must be non-empty. Non-matching algorithms cause a parse error.
|
||||
//
|
||||
// Deprecated: call ParseSignedJSON or ParseSignedCompact depending on which serialization you expect.
|
||||
func ParseSigned(
|
||||
signature string,
|
||||
signatureAlgorithms []SignatureAlgorithm,
|
||||
@@ -108,6 +119,9 @@ func ParseSigned(
|
||||
// Verify() after parsing to validate the signature and obtain the payload.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7515#section-7.1
|
||||
//
|
||||
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
|
||||
// It must be non-empty. Non-matching algorithms cause a parse error.
|
||||
func ParseSignedCompact(
|
||||
signature string,
|
||||
signatureAlgorithms []SignatureAlgorithm,
|
||||
@@ -125,6 +139,9 @@ func ParseSignedCompact(
|
||||
// parsing. Call Verify() after parsing to validate the signature and obtain the payload.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
|
||||
//
|
||||
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
|
||||
// It must be non-empty. Any non-allowed algorithms will cause parsing to fail.
|
||||
func ParseDetached(
|
||||
signature string,
|
||||
payload []byte,
|
||||
@@ -157,7 +174,11 @@ func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature
|
||||
authData.WriteString(signature.original.Protected.base64())
|
||||
} else if signature.protected != nil {
|
||||
protectedHeader = signature.protected
|
||||
authData.WriteString(base64.RawURLEncoding.EncodeToString(mustSerializeJSON(protectedHeader)))
|
||||
serializedProtected, err := json.Marshal(protectedHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authData.WriteString(base64.RawURLEncoding.EncodeToString(serializedProtected))
|
||||
}
|
||||
|
||||
needsBase64 := true
|
||||
@@ -180,9 +201,13 @@ func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature
|
||||
return authData.Bytes(), nil
|
||||
}
|
||||
|
||||
// ParseSignedJSON parses a message in JWS JSON Serialization.
|
||||
// ParseSignedJSON parses a message in JWS JSON Serialization. Messages with
|
||||
// more than [MaxSignatures] signatures are rejected.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7515#section-7.2
|
||||
//
|
||||
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
|
||||
// It must be non-empty. Non-matching algorithms cause a parse error.
|
||||
func ParseSignedJSON(
|
||||
input string,
|
||||
signatureAlgorithms []SignatureAlgorithm,
|
||||
@@ -236,6 +261,9 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
if parsed.Payload == nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: missing payload in JWS message")
|
||||
}
|
||||
if len(parsed.Signatures) > MaxSignatures {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: too many signatures in JWS; maximum is %d", MaxSignatures)
|
||||
}
|
||||
|
||||
obj := &JSONWebSignature{
|
||||
payload: parsed.Payload.bytes(),
|
||||
@@ -245,14 +273,13 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
if len(parsed.Signatures) == 0 {
|
||||
// No signatures array, must be flattened serialization
|
||||
signature := Signature{}
|
||||
if parsed.Protected != nil && len(parsed.Protected.bytes()) > 0 {
|
||||
signature.protected = &rawHeader{}
|
||||
err := json.Unmarshal(parsed.Protected.bytes(), signature.protected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prot, err := parseProtectedHeader(parsed.Protected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature.protected = prot
|
||||
|
||||
// Check that there is not a nonce in the unprotected header
|
||||
if parsed.Header != nil && parsed.Header.getNonce() != "" {
|
||||
return nil, ErrUnprotectedNonce
|
||||
@@ -275,17 +302,17 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
Signature: parsed.Signature,
|
||||
}
|
||||
|
||||
var err error
|
||||
signature.Header, err = signature.mergedHeaders().sanitized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alg := SignatureAlgorithm(signature.Header.Algorithm)
|
||||
mergedHeaders := signature.mergedHeaders()
|
||||
alg := mergedHeaders.getSignatureAlgorithm()
|
||||
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
|
||||
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
|
||||
}
|
||||
|
||||
signature.Header, err = mergedHeaders.sanitized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if signature.header != nil {
|
||||
signature.Unprotected, err = signature.header.sanitized()
|
||||
if err != nil {
|
||||
@@ -310,30 +337,31 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
}
|
||||
|
||||
for i, sig := range parsed.Signatures {
|
||||
if sig.Protected != nil && len(sig.Protected.bytes()) > 0 {
|
||||
obj.Signatures[i].protected = &rawHeader{}
|
||||
err := json.Unmarshal(sig.Protected.bytes(), obj.Signatures[i].protected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prot, err := parseProtectedHeader(sig.Protected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj.Signatures[i].protected = prot
|
||||
|
||||
// Check that there is not a nonce in the unprotected header
|
||||
if sig.Header != nil && sig.Header.getNonce() != "" {
|
||||
return nil, ErrUnprotectedNonce
|
||||
}
|
||||
|
||||
var err error
|
||||
obj.Signatures[i].Header, err = obj.Signatures[i].mergedHeaders().sanitized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Signatures[i].header = sig.Header
|
||||
|
||||
alg := SignatureAlgorithm(obj.Signatures[i].Header.Algorithm)
|
||||
mergedHeaders := obj.Signatures[i].mergedHeaders()
|
||||
alg := mergedHeaders.getSignatureAlgorithm()
|
||||
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
|
||||
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
|
||||
}
|
||||
|
||||
obj.Signatures[i].Header, err = mergedHeaders.sanitized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if obj.Signatures[i].header != nil {
|
||||
obj.Signatures[i].Unprotected, err = obj.Signatures[i].header.sanitized()
|
||||
if err != nil {
|
||||
@@ -359,7 +387,6 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
// Copy value of sig
|
||||
original := sig
|
||||
|
||||
obj.Signatures[i].header = sig.Header
|
||||
obj.Signatures[i].original = &original
|
||||
}
|
||||
|
||||
@@ -424,7 +451,10 @@ func (obj JSONWebSignature) compactSerialize(detached bool) (string, error) {
|
||||
return "", ErrNotSupported
|
||||
}
|
||||
|
||||
serializedProtected := mustSerializeJSON(obj.Signatures[0].protected)
|
||||
serializedProtected, err := json.Marshal(obj.Signatures[0].protected)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
if !detached {
|
||||
@@ -449,14 +479,23 @@ func (obj JSONWebSignature) DetachedCompactSerialize() (string, error) {
|
||||
}
|
||||
|
||||
// FullSerialize serializes an object using the full JSON serialization format.
|
||||
//
|
||||
// It returns "" if obj cannot be serialized.
|
||||
func (obj JSONWebSignature) FullSerialize() string {
|
||||
raw := rawJSONWebSignature{
|
||||
Payload: newBuffer(obj.payload),
|
||||
}
|
||||
|
||||
if len(obj.Signatures) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(obj.Signatures) == 1 {
|
||||
if obj.Signatures[0].protected != nil {
|
||||
serializedProtected := mustSerializeJSON(obj.Signatures[0].protected)
|
||||
serializedProtected, err := json.Marshal(obj.Signatures[0].protected)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
raw.Protected = newBuffer(serializedProtected)
|
||||
}
|
||||
raw.Header = obj.Signatures[0].header
|
||||
@@ -470,10 +509,18 @@ func (obj JSONWebSignature) FullSerialize() string {
|
||||
}
|
||||
|
||||
if signature.protected != nil {
|
||||
raw.Signatures[i].Protected = newBuffer(mustSerializeJSON(signature.protected))
|
||||
serializedProtected, err := json.Marshal(signature.protected)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
raw.Signatures[i].Protected = newBuffer(serializedProtected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string(mustSerializeJSON(raw))
|
||||
out, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ var ErrNotValidYet = errors.New("go-jose/go-jose/jwt: validation failed, token n
|
||||
var ErrExpired = errors.New("go-jose/go-jose/jwt: validation failed, token is expired (exp)")
|
||||
|
||||
// ErrIssuedInTheFuture indicates that the iat field is in the future.
|
||||
var ErrIssuedInTheFuture = errors.New("go-jose/go-jose/jwt: validation field, token issued in the future (iat)")
|
||||
var ErrIssuedInTheFuture = errors.New("go-jose/go-jose/jwt: validation failed, token issued in the future (iat)")
|
||||
|
||||
// ErrInvalidContentType indicates that token requires JWT cty header.
|
||||
var ErrInvalidContentType = errors.New("go-jose/go-jose/jwt: expected content type to be JWT (cty header)")
|
||||
|
||||
+12
-17
@@ -85,7 +85,12 @@ func (t *NestedJSONWebToken) Decrypt(decryptionKey interface{}) (*JSONWebToken,
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// ParseSigned parses token from JWS form.
|
||||
// ParseSigned parses a JWT in the JWS Compact Serialization.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7515#section-3.1
|
||||
//
|
||||
// The signatureAlgorithms parameter contains the allowable algorithms in the "alg" header parameter.
|
||||
// It must be non-empty. Non-matching algorithms cause a parse error.
|
||||
func ParseSigned(s string, signatureAlgorithms []jose.SignatureAlgorithm) (*JSONWebToken, error) {
|
||||
sig, err := jose.ParseSignedCompact(s, signatureAlgorithms)
|
||||
if err != nil {
|
||||
@@ -142,13 +147,10 @@ func parseEncryptedCompact(
|
||||
return enc, nil
|
||||
}
|
||||
|
||||
// ParseEncrypted parses token from JWE form.
|
||||
// ParseEncrypted parses a JWT in the JWE Compact Serialization.
|
||||
//
|
||||
// The keyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
|
||||
// header parameters respectively. They must be nonempty, and each "alg" or "enc" header in
|
||||
// parsed data must contain a value that is present in the corresponding parameter. That
|
||||
// includes the protected and unprotected headers as well as all recipients. To accept
|
||||
// multiple algorithms, pass a slice of all the algorithms you want to accept.
|
||||
// The keyAlgorithms and contentEncryption parameters contain the allowable algorithms in the "alg" and "enc"
|
||||
// header parameters respectively. They must be non-empty. Non-matching algorithms cause a parse error.
|
||||
func ParseEncrypted(s string,
|
||||
keyAlgorithms []jose.KeyAlgorithm,
|
||||
contentEncryption []jose.ContentEncryption,
|
||||
@@ -164,17 +166,10 @@ func ParseEncrypted(s string,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseSignedAndEncrypted parses signed-then-encrypted token from JWE form.
|
||||
// ParseSignedAndEncrypted parses a signed-then-encrypted token in the JWE Compact Serialization.
|
||||
//
|
||||
// The encryptionKeyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
|
||||
// header parameters, respectively, of the outer JWE. They must be nonempty, and each "alg" or "enc"
|
||||
// header in parsed data must contain a value that is present in the corresponding parameter. That
|
||||
// includes the protected and unprotected headers as well as all recipients. To accept
|
||||
// multiple algorithms, pass a slice of all the algorithms you want to accept.
|
||||
//
|
||||
// The signatureAlgorithms parameter is used to validate the "alg" header parameter of the
|
||||
// inner JWS. It must be nonempty, and the "alg" header in the inner JWS must contain a value
|
||||
// that is present in the parameter.
|
||||
// The encryptionKeyAlgorithms, contentEncryption, and signatureAlgorithms parameters contain the allowable
|
||||
// algorithms. They must be non-empty. Non-matching algorithms cause a parse error.
|
||||
func ParseSignedAndEncrypted(s string,
|
||||
encryptionKeyAlgorithms []jose.KeyAlgorithm,
|
||||
contentEncryption []jose.ContentEncryption,
|
||||
|
||||
+43
-37
@@ -18,6 +18,7 @@ package jose
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
@@ -210,13 +211,20 @@ type Header struct {
|
||||
|
||||
// Certificates verifies & returns the certificate chain present
|
||||
// in the x5c header field of a message, if one was present. Returns
|
||||
// an error if there was no x5c header present or the chain could
|
||||
// an error if there was no x5c header present, any certificate in the
|
||||
// chain has an RSA key larger than [MaxRSAModulusBits], or the chain could
|
||||
// not be validated with the given verify options.
|
||||
func (h Header) Certificates(opts x509.VerifyOptions) ([][]*x509.Certificate, error) {
|
||||
if len(h.certificates) == 0 {
|
||||
return nil, ErrMissingX5cHeader
|
||||
}
|
||||
|
||||
for _, cert := range h.certificates {
|
||||
if pub, ok := cert.PublicKey.(*rsa.PublicKey); ok && pub.N.BitLen() > MaxRSAModulusBits {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid RSA key in x5c certificate, modulus too large")
|
||||
}
|
||||
}
|
||||
|
||||
leaf := h.certificates[0]
|
||||
if opts.Intermediates == nil {
|
||||
opts.Intermediates = x509.NewCertPool()
|
||||
@@ -297,9 +305,10 @@ func (parsed rawHeader) getEPK() (*JSONWebKey, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var epk *JSONWebKey
|
||||
err := json.Unmarshal(*v, &epk)
|
||||
if err != nil {
|
||||
// RFC 7518 Section 4.6.1.1: the "epk" header is the ephemeral public key
|
||||
// of the originator. Reject private key material before parsing it.
|
||||
epk := new(JSONWebKey)
|
||||
if err := epk.unmarshalJSON(*v, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return epk, nil
|
||||
@@ -325,20 +334,6 @@ func (parsed rawHeader) getTag() (*byteBuffer, error) {
|
||||
return parsed.getByteBuffer(headerTag)
|
||||
}
|
||||
|
||||
// getJWK extracts parsed "jwk" from the raw JSON.
|
||||
func (parsed rawHeader) getJWK() (*JSONWebKey, error) {
|
||||
v := parsed[headerJWK]
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var jwk *JSONWebKey
|
||||
err := json.Unmarshal(*v, &jwk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jwk, nil
|
||||
}
|
||||
|
||||
// getCritical extracts parsed "crit" from the raw JSON. If omitted, it
|
||||
// returns an empty slice.
|
||||
func (parsed rawHeader) getCritical() ([]string, error) {
|
||||
@@ -424,10 +419,14 @@ func (parsed rawHeader) sanitized() (h Header, err error) {
|
||||
}
|
||||
switch k {
|
||||
case headerJWK:
|
||||
var jwk *JSONWebKey
|
||||
err = json.Unmarshal(*v, &jwk)
|
||||
// RFC 7515 Section 4.1.3 and RFC 7516 Section 4.1.5: the "jwk"
|
||||
// header is the public key that corresponds to the key used to
|
||||
// sign the JWS or encrypt the JWE. Reject private key material
|
||||
// before parsing it.
|
||||
jwk := new(JSONWebKey)
|
||||
err = jwk.unmarshalJSON(*v, false)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal JWK: %v: %#v", err, string(*v))
|
||||
err = fmt.Errorf("failed to unmarshal JWK: %w", err)
|
||||
return
|
||||
}
|
||||
h.JSONWebKey = jwk
|
||||
@@ -499,22 +498,8 @@ func parseCertificateChain(chain []string) ([]*x509.Certificate, error) {
|
||||
}
|
||||
|
||||
func (parsed rawHeader) isSet(k HeaderKey) bool {
|
||||
dvr := parsed[k]
|
||||
if dvr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var dv interface{}
|
||||
err := json.Unmarshal(*dvr, &dv)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if dvStr, ok := dv.(string); ok {
|
||||
return dvStr != ""
|
||||
}
|
||||
|
||||
return true
|
||||
_, ok := parsed[k]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Merge headers from src into dst, giving precedence to headers from l.
|
||||
@@ -564,3 +549,24 @@ func makeRawMessage(b []byte) *json.RawMessage {
|
||||
rm := json.RawMessage(b)
|
||||
return &rm
|
||||
}
|
||||
|
||||
// parseProtectedHeader parses a `byteBuffer` into a `*rawHeader`.
|
||||
//
|
||||
// The byteBuffer represents a base64-decoded "protected" header, which
|
||||
// is specified to contain a JSON object.
|
||||
//
|
||||
// If the input is nil or empty, returns `nil, nil`.
|
||||
//
|
||||
// `rawHeader` is a map type and so can be nil. When this function
|
||||
// returns a non-nil `*rawHeader`, the map is guaranteed to be non-nil.
|
||||
func parseProtectedHeader(in *byteBuffer) (*rawHeader, error) {
|
||||
if in == nil || len(in.bytes()) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var header rawHeader
|
||||
err := json.Unmarshal(in.bytes(), &header)
|
||||
if err != nil || header == nil {
|
||||
return nil, errors.New("go-jose/go-jose: invalid protected header")
|
||||
}
|
||||
return &header, nil
|
||||
}
|
||||
|
||||
+4
-1
@@ -321,7 +321,10 @@ func (ctx *genericSigner) Sign(payload []byte) (*JSONWebSignature, error) {
|
||||
protected[k] = v
|
||||
}
|
||||
|
||||
serializedProtected := mustSerializeJSON(protected)
|
||||
serializedProtected, err := json.Marshal(protected)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: Error marshaling JSON: %s", err)
|
||||
}
|
||||
needsBase64 := true
|
||||
|
||||
if b64, ok := protected[headerB64]; ok {
|
||||
|
||||
+1
-1
@@ -421,7 +421,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid P2S: %v", err)
|
||||
}
|
||||
if p2s == nil || len(p2s.data) == 0 {
|
||||
if p2s == nil || len(p2s.bytes()) == 0 {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid P2S: must be present")
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# This containerfile can be used to run the go-jose tests and cli
|
||||
|
||||
FROM golang:1.27-trixie
|
||||
ENV GOTOOLCHAIN=local
|
||||
|
||||
COPY . /src
|
||||
WORKDIR /src
|
||||
|
||||
# Install the jose-util cli, which will also build the library
|
||||
RUN go build -o /usr/local/bin/jose-util ./jose-util
|
||||
|
||||
# By default, run the tests
|
||||
CMD ["go", "test", "./..."]
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Threat model
|
||||
|
||||
## What this project does and where untrusted input enters
|
||||
go-jose is a Go implementation of JOSE family of algorithms.
|
||||
|
||||
It is a library which will parse attacker-controlled data in network-reachable
|
||||
services.
|
||||
|
||||
## Components that matter most / least
|
||||
The primary attack surface of interest is parsing and validating JWS, JWE, JWK,
|
||||
etc when this library is used to implement authentication and authorization.
|
||||
|
||||
## How to exercise it
|
||||
Testing is primarily done by Golang unit tests, which can be run with the
|
||||
tests.Container file's default CMD.
|
||||
|
||||
## How you rate severity
|
||||
Any authentication bypass enabled by a go-jose issue is a high+ severity.
|
||||
|
||||
Denial-of-service attacks are of interest when the functions may be used in a protocol like OIDC, ACME, etc
|
||||
which could have a server processing attacker-controlled data.
|
||||
|
||||
## Anything to leave alone
|
||||
Any bug which only affects the jose-util CLI tool, but it can be used to exercise the library.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user