mirror of
https://github.com/docker/cli.git
synced 2026-11-06 21:18:28 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c3c00dd7f |
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
// https://docs.renovatebot.com/modules/manager/github-actions/
|
||||
// https://docs.renovatebot.com/presets-helpers/
|
||||
"helpers:pinGitHubActionDigestsToSemver",
|
||||
":gitSignOff"
|
||||
],
|
||||
"gomod": {
|
||||
"managerFilePatterns": ["/(^|/)vendor\\.mod$/"]
|
||||
},
|
||||
"postUpdateOptions": ["gomodTidy"],
|
||||
"minimumReleaseAge": "3 days",
|
||||
"internalChecksFilter": "strict",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessagePrefix": "vendor:"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["cmd/docker-trust/go.mod"],
|
||||
// The trust module does not vendor dependencies; vendor/ belongs to the root module.
|
||||
"postUpdateOptions": ["gomodSkipVendor"],
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessagePrefix": "cmd/docker-trust:"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"groupName": "golang.org/x packages",
|
||||
"groupSlug": "golang-x",
|
||||
"matchDepNames": ["/^golang\\.org\\/x\\//"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"addLabels": ["area/ci", "status/2-code-review"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "docker GitHub Actions",
|
||||
"groupSlug": "docker-actions",
|
||||
"matchDepNames": ["/^docker\\//"]
|
||||
},
|
||||
{
|
||||
// Keep all the google.golang.org/genproto modules at the same version.
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"groupName": "google.golang.org/genproto packages",
|
||||
"groupSlug": "google-genproto",
|
||||
"matchDepNames": ["/^google\\.golang\\.org\\/genproto(\\/|$)/"],
|
||||
"schedule": ["every month"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -33,6 +33,14 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -63,9 +71,17 @@ jobs:
|
||||
- ""
|
||||
- glibc
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
@@ -91,7 +107,7 @@ jobs:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
@@ -130,6 +146,14 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -154,9 +178,17 @@ jobs:
|
||||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
|
||||
@@ -64,18 +64,18 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.10"
|
||||
go-version: "1.26.8"
|
||||
cache: false
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
@@ -37,12 +37,19 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- master # moby/moby-bin:master
|
||||
- rc # latest rc
|
||||
- 29 # latest
|
||||
- 28 # latest - 1
|
||||
- 25 # mirantis lts
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -66,7 +73,7 @@ jobs:
|
||||
docker info
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
-
|
||||
name: Run ${{ matrix.target }}
|
||||
run: |
|
||||
|
||||
@@ -20,6 +20,14 @@ jobs:
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
- name: Save event context
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
@@ -35,7 +43,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload context
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pr-review-context
|
||||
path: context/
|
||||
|
||||
@@ -28,9 +28,17 @@ jobs:
|
||||
ctn:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
@@ -58,6 +66,14 @@ jobs:
|
||||
- macos-15 # macOS 15 on arm64 (Apple Silicon M1)
|
||||
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -68,7 +84,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.10"
|
||||
go-version: "1.26.8"
|
||||
cache: false
|
||||
-
|
||||
name: Test
|
||||
|
||||
@@ -13,6 +13,14 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
- name: Validate milestone matches VERSION
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
|
||||
@@ -18,6 +18,14 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
- name: Missing `area/` label
|
||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
run: |
|
||||
@@ -39,6 +47,14 @@ jobs:
|
||||
PR_BODY: |
|
||||
${{ github.event.pull_request.body }}
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
- name: Check changelog description
|
||||
run: |
|
||||
# Extract the `markdown changelog` note code block
|
||||
@@ -75,6 +91,14 @@ jobs:
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
|
||||
# [X.Y backport] Some change that needs backporting to X.Y
|
||||
# [X.Y] Change directly targeting the X.Y branch
|
||||
|
||||
@@ -36,6 +36,14 @@ jobs:
|
||||
- validate-vendor
|
||||
- update-authors # ensure authors update target runs fine
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Run
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
@@ -46,6 +54,14 @@ jobs:
|
||||
validate-md:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -74,6 +90,14 @@ jobs:
|
||||
- yamldocs # ensure yamldocs target runs fine
|
||||
- manpages # ensure manpages target runs fine
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -91,6 +115,14 @@ jobs:
|
||||
GOPATH: ${{ github.workspace }}
|
||||
GO111MODULE: off
|
||||
steps:
|
||||
- name: Configure credentials
|
||||
continue-on-error: true
|
||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
||||
with:
|
||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
||||
profile: administrator
|
||||
profile-region: us-east-1
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -101,7 +133,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.10"
|
||||
go-version: "1.26.8"
|
||||
cache: false
|
||||
-
|
||||
name: Run gocompat check
|
||||
|
||||
+1
-1
@@ -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.10"
|
||||
go: "1.26.8"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
|
||||
+4
-4
@@ -8,11 +8,11 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.26.10
|
||||
ARG GO_VERSION=1.26.8
|
||||
|
||||
# XX_VERSION specifies the version of the xx utility to use.
|
||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
||||
ARG XX_VERSION=1.10.0
|
||||
ARG XX_VERSION=1.9.0
|
||||
|
||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
||||
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
|
||||
@@ -25,12 +25,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.38.0
|
||||
ARG BUILDX_VERSION=0.34.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.6.0
|
||||
ARG COMPOSE_VERSION=v5.1.3
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[](https://github.com/docker/cli/actions?query=workflow%3Atest)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
|
||||
[](https://codecov.io/gh/docker/cli)
|
||||
[](https://community.tracebit.com/supplychainattacks)
|
||||
|
||||
## About
|
||||
|
||||
|
||||
@@ -30,11 +30,7 @@ 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,42 +7,17 @@ const (
|
||||
indent = " "
|
||||
)
|
||||
|
||||
// PrintMessages groups messages by type and writes them to out. Generic messages
|
||||
// appear first, without a header or indentation, followed by next steps under a
|
||||
// shared "What's next:" header. Order within each type is preserved.
|
||||
// Messages with no lines or an unknown type produce no output.
|
||||
func PrintMessages(out io.Writer, messages []Response, evaluate func(Response) []string) {
|
||||
grouped := make(map[ResponseType][]string)
|
||||
for _, message := range messages {
|
||||
lines := evaluate(message)
|
||||
grouped[message.Type] = append(grouped[message.Type], lines...)
|
||||
}
|
||||
for _, responseType := range []ResponseType{GenericMessage, NextSteps} {
|
||||
lines := grouped[responseType]
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
header, prefix := "\n", ""
|
||||
if responseType == NextSteps {
|
||||
header, prefix = whatsNext, indent
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(out, header)
|
||||
for _, line := range lines {
|
||||
_, _ = io.WriteString(out, prefix)
|
||||
_, _ = io.WriteString(out, line)
|
||||
_, _ = io.WriteString(out, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintNextSteps renders list of [NextSteps] messages and writes them
|
||||
// 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
|
||||
})
|
||||
if len(messages) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(out, whatsNext)
|
||||
for _, msg := range messages {
|
||||
_, _ = io.WriteString(out, indent)
|
||||
_, _ = io.WriteString(out, msg)
|
||||
_, _ = io.WriteString(out, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,85 +8,7 @@ import (
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
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) {
|
||||
func TestPrintHookMessages(t *testing.T) {
|
||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
||||
|
||||
tests := []struct {
|
||||
@@ -112,11 +34,6 @@ func TestPrintNextSteps(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,7 +7,6 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
@@ -44,16 +43,7 @@ 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)
|
||||
}
|
||||
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
|
||||
return strings.SplitN(out, "\n", maxMessages), nil
|
||||
}
|
||||
|
||||
var ErrHookTemplateParse = errors.New("failed to parse hook template")
|
||||
|
||||
@@ -33,17 +33,7 @@ func TestParseTemplate(t *testing.T) {
|
||||
{
|
||||
doc: "empty template",
|
||||
template: "",
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "only empty lines",
|
||||
template: "\n\n",
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "whitespace-only lines",
|
||||
template: " \n\t\n ",
|
||||
expectedOutput: nil,
|
||||
expectedOutput: []string{""},
|
||||
},
|
||||
{
|
||||
doc: "plain message",
|
||||
|
||||
@@ -49,18 +49,11 @@ func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, sub
|
||||
}
|
||||
|
||||
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
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
|
||||
})
|
||||
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
|
||||
}
|
||||
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []hooks.Response {
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -71,17 +64,17 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
}
|
||||
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
var collected []hooks.Response
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (hooks.Response, error) {
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
|
||||
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
||||
if !matched {
|
||||
return hooks.Response{}, nil
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
||||
if err != nil {
|
||||
return hooks.Response{}, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
resp, err := p.RunHook(ctx, hooks.Request{
|
||||
@@ -90,23 +83,29 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
CommandError: cmdErrorMessage,
|
||||
})
|
||||
if err != nil {
|
||||
return hooks.Response{}, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var message hooks.Response
|
||||
if err := json.Unmarshal(resp, &message); err != nil {
|
||||
return hooks.Response{}, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
}
|
||||
|
||||
if message.Type != hooks.NextSteps && message.Type != hooks.GenericMessage {
|
||||
return hooks.Response{}, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
// currently the only hook type
|
||||
if message.Type != hooks.NextSteps {
|
||||
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
}
|
||||
|
||||
return message, nil
|
||||
messages, err = hooks.ParseTemplate(message.Template, subCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return messages, true, nil
|
||||
}
|
||||
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
message, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
if err != nil {
|
||||
// skip misbehaving plugins, but don't halt execution
|
||||
logrus.WithFields(logrus.Fields{
|
||||
@@ -115,13 +114,38 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
}).Debug("Plugin hook invocation failed")
|
||||
continue
|
||||
}
|
||||
if message.Template == "" {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
collected = append(collected, message)
|
||||
var appended bool
|
||||
nextSteps, appended = appendNextSteps(nextSteps, messages)
|
||||
if !appended {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"plugin": pluginName,
|
||||
}).Debug("Plugin responded with an empty hook message; ignoring")
|
||||
}
|
||||
}
|
||||
return collected
|
||||
return nextSteps
|
||||
}
|
||||
|
||||
// appendNextSteps appends the processed hook output to the nextSteps slice.
|
||||
// If the processed hook output is empty, it is not appended.
|
||||
// Empty lines are not stripped if there's at least one non-empty line.
|
||||
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
||||
empty := true
|
||||
for _, l := range processed {
|
||||
if strings.TrimSpace(l) != "" {
|
||||
empty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if empty {
|
||||
return nextSteps, false
|
||||
}
|
||||
|
||||
return append(nextSteps, processed...), true
|
||||
}
|
||||
|
||||
// pluginMatch takes a plugin configuration and a string representing the
|
||||
|
||||
@@ -2,11 +2,6 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
@@ -263,6 +258,38 @@ func TestMatchHookConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendNextSteps(t *testing.T) {
|
||||
testCases := []struct {
|
||||
processed []string
|
||||
expectedOut []string
|
||||
}{
|
||||
{
|
||||
processed: []string{},
|
||||
expectedOut: []string{},
|
||||
},
|
||||
{
|
||||
processed: []string{"", ""},
|
||||
expectedOut: []string{},
|
||||
},
|
||||
{
|
||||
processed: []string{"Some hint", "", "Some other hint"},
|
||||
expectedOut: []string{"Some hint", "", "Some other hint"},
|
||||
},
|
||||
{
|
||||
processed: []string{"Hint 1", "Hint 2"},
|
||||
expectedOut: []string{"Hint 1", "Hint 2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got, appended := appendNextSteps([]string{}, tc.processed)
|
||||
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
|
||||
assert.Check(t, is.Equal(appended, len(got) > 0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
@@ -309,7 +336,7 @@ func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
|
||||
// On success, error-hooks should not match, so the plugin
|
||||
// binary is never looked up and no results are returned.
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root,
|
||||
context.Background(), cfg, root, sub,
|
||||
"build", map[string]string{}, "",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
@@ -322,7 +349,7 @@ func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
|
||||
root.AddCommand(sub)
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root,
|
||||
context.Background(), cfg, root, sub,
|
||||
"build", map[string]string{}, "some error",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
@@ -341,108 +368,8 @@ func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
|
||||
cancel() // cancel immediately
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
ctx, cfg, root,
|
||||
ctx, cfg, root, sub,
|
||||
"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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.26
|
||||
|
||||
package metadata
|
||||
|
||||
const (
|
||||
@@ -38,7 +35,4 @@ type Metadata struct {
|
||||
URL string `json:",omitempty"`
|
||||
// Hidden hides the plugin in completion and help message output.
|
||||
Hidden bool `json:",omitempty"`
|
||||
// Features declares optional contracts supported by the plugin, keyed by
|
||||
// feature name.
|
||||
Features map[string]any `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ type DockerCli struct {
|
||||
serverInfo ServerInfo
|
||||
contextStore store.Store
|
||||
currentContext string
|
||||
contextResolver func(*DockerCli) (string, error)
|
||||
init sync.Once
|
||||
initErr error
|
||||
dockerEndpoint docker.Endpoint
|
||||
@@ -209,8 +208,6 @@ func (cli *DockerCli) HooksEnabled() bool {
|
||||
|
||||
// Initialize the dockerCli runs initialization that must happen after command
|
||||
// line flags are parsed.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
|
||||
for _, o := range ops {
|
||||
if err := o(cli); err != nil {
|
||||
@@ -260,16 +257,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
},
|
||||
}
|
||||
|
||||
if cli.contextResolver != nil {
|
||||
contextName, err := cli.contextResolver(cli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contextName != "" {
|
||||
cli.currentContext = contextName
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(krissetto): pass ctx to the funcs instead of using this
|
||||
if cli.enableGlobalMeter {
|
||||
cli.createGlobalMeterProvider(cli.baseCtx)
|
||||
|
||||
@@ -84,19 +84,6 @@ func WithDefaultContextStoreConfig() CLIOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithContextResolver overrides context selection during [DockerCli.Initialize].
|
||||
// The resolver runs after configuration and the context store are loaded, but
|
||||
// before telemetry and endpoint initialization.
|
||||
// An empty result preserves normal context selection; an error aborts
|
||||
// initialization.
|
||||
// The resolver must not initialize the API client.
|
||||
func WithContextResolver(resolve func(*DockerCli) (string, error)) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contextResolver = resolve
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPIClient configures the cli to use the given API client.
|
||||
func WithAPIClient(c client.APIClient) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
|
||||
@@ -325,64 +325,6 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
assert.Check(t, cli.ContextStore() != nil)
|
||||
}
|
||||
|
||||
func TestInitializeContextResolver(t *testing.T) {
|
||||
resolverErr := errors.New("resolver failed")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
context string
|
||||
err error
|
||||
}{
|
||||
{name: "resolved context", context: "resolved"},
|
||||
{name: "normal selection"},
|
||||
{name: "resolver failure", err: resolverErr},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
originalConfigDir := config.Dir()
|
||||
t.Cleanup(func() { config.SetDir(originalConfigDir) })
|
||||
config.SetDir(t.TempDir())
|
||||
|
||||
configDir := t.TempDir()
|
||||
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
|
||||
cfg.Features = map[string]string{"cloud": "foobar"}
|
||||
assert.NilError(t, cfg.Save())
|
||||
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
|
||||
var loadedConfig *configfile.ConfigFile
|
||||
calls := 0
|
||||
|
||||
err = cli.Initialize(&flags.ClientOptions{ConfigDir: configDir, Context: "original"},
|
||||
WithContextResolver(func(cli *DockerCli) (string, error) {
|
||||
calls++
|
||||
assert.Equal(t, config.Dir(), configDir)
|
||||
loadedConfig = cli.ConfigFile()
|
||||
assert.Equal(t, loadedConfig.Features["cloud"], "foobar")
|
||||
assert.Assert(t, cli.ContextStore() != nil)
|
||||
assert.Assert(t, cli.client == nil)
|
||||
return tc.context, tc.err
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, calls, 1)
|
||||
assert.Assert(t, cli.ConfigFile() == loadedConfig)
|
||||
assert.Assert(t, cli.client == nil)
|
||||
|
||||
if tc.err != nil {
|
||||
assert.ErrorIs(t, err, tc.err)
|
||||
return
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
|
||||
wantContext := tc.context
|
||||
if wantContext == "" {
|
||||
wantContext = "original"
|
||||
}
|
||||
assert.Equal(t, cli.CurrentContext(), wantContext)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHooksEnabled(t *testing.T) {
|
||||
t.Run("disabled by default", func(t *testing.T) {
|
||||
// Make sure we don't depend on any existing ~/.docker/config.json
|
||||
|
||||
@@ -260,11 +260,8 @@ 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 {
|
||||
if options.quiet {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
}
|
||||
return err
|
||||
if err != nil && options.quiet {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unable to prepare context: path %q not found", options.context)
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -174,35 +172,6 @@ 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
-10
@@ -5,13 +5,13 @@ go 1.26.0
|
||||
require (
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v29.8.2+incompatible
|
||||
github.com/docker/cli v29.7.2+incompatible
|
||||
github.com/docker/cli-docs-tool v0.11.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/go-connections v0.8.2
|
||||
github.com/docker/go-connections v0.8.1
|
||||
github.com/fvbommel/sortorder v1.2.0
|
||||
github.com/moby/moby/api v1.56.1
|
||||
github.com/moby/moby/client v0.6.2
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/sirupsen/logrus v1.10.2
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
@@ -66,11 +66,11 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/net v0.60.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/crypto v0.56.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
|
||||
+21
-20
@@ -1,5 +1,5 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -42,8 +42,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ=
|
||||
github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
|
||||
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
|
||||
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
@@ -54,8 +54,8 @@ github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
|
||||
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
|
||||
github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw=
|
||||
@@ -133,10 +133,10 @@ github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KH
|
||||
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/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.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
@@ -236,26 +236,27 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli"
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config"
|
||||
contextdocker "github.com/docker/cli/cli/context/docker"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// processCloud resolves the global --cloud[=NAME] option after configuration is
|
||||
// loaded, but before telemetry and endpoint initialization.
|
||||
// The option appears in help and completion only when the selected provider is
|
||||
// installed, valid, and declares support for the resolver contract.
|
||||
// The provider defaults to the offload plugin; features.cloud in the effective
|
||||
// Docker config.json can override the plugin name, not its executable path.
|
||||
// Explicit use also requires the provider's capability declaration.
|
||||
// Discovery for visibility does not provision a context or connect to a daemon.
|
||||
//
|
||||
// # Provider contract
|
||||
//
|
||||
// The provider must include "Features": {"cloud-context-resolver": true} in its
|
||||
// docker-cli-plugin-metadata response to declare support for this contract.
|
||||
// Older plugins without this declaration leave the flag hidden and cannot be
|
||||
// used for resolution.
|
||||
// The CLI uses normal plugin discovery and invokes:
|
||||
//
|
||||
// docker-<provider> --config=<dir> <provider> __resolve-context -- <name>
|
||||
//
|
||||
// Bare --cloud and --cloud= pass default as the target name.
|
||||
// The effective config directory is also passed as DOCKER_CONFIG.
|
||||
// The provider must provision into that context store without changing the saved
|
||||
// current context or recursively forwarding --cloud.
|
||||
// It inherits the environment and sends progress and prompts to stderr. Stdin
|
||||
// is forwarded only when both stdin and stderr are terminals with file handles;
|
||||
// wrapped Windows consoles use the corresponding standard handles. Otherwise
|
||||
// the provider receives EOF and must not prompt or open a terminal
|
||||
// separately. Piped and redirected input belongs to the requested command.
|
||||
// Stdout must contain exactly one JSON object:
|
||||
//
|
||||
// {"DOCKER_CONTEXT":"provisioned-context"}
|
||||
//
|
||||
// The returned context must exist, must not be the virtual default context, and
|
||||
// must have a Docker endpoint with a nonempty host.
|
||||
// Resolver failures abort command execution without falling back to another
|
||||
// context.
|
||||
// Cancellation terminates the resolver process; provisioning subprocesses and
|
||||
// resource cleanup remain the provider's responsibility.
|
||||
// Downstream plugins receive --context=<resolved-name> instead of the global
|
||||
// --cloud option.
|
||||
// The provider implementation ships separately and must support this operation
|
||||
// before enabling the integration.
|
||||
func processCloud(ctx context.Context, dockerCli *command.DockerCli, cmd *cobra.Command, args, osArgs []string) ([]string, error) {
|
||||
if !cmd.Flags().Changed("cloud") {
|
||||
return osArgs, nil
|
||||
}
|
||||
if cmd.Flags().Changed("context") || cmd.Flags().Changed("host") {
|
||||
return osArgs, errors.New("conflicting options: cannot specify --cloud together with --context or --host")
|
||||
}
|
||||
|
||||
help, err := cloudHelpRequest(cmd, args)
|
||||
if err != nil {
|
||||
return osArgs, err
|
||||
}
|
||||
|
||||
var contextName string
|
||||
if !help {
|
||||
name, _ := cmd.Flags().GetString("cloud")
|
||||
if name == "" {
|
||||
name = "default"
|
||||
}
|
||||
|
||||
contextName, err = resolveCloudContext(ctx, dockerCli, cmd, name)
|
||||
if err != nil {
|
||||
return osArgs, fmt.Errorf("--cloud: %w", err)
|
||||
}
|
||||
if err := cmd.Flags().Set("context", contextName); err != nil {
|
||||
return osArgs, err
|
||||
}
|
||||
}
|
||||
|
||||
return cloudPluginArgs(cmd, osArgs, contextName)
|
||||
}
|
||||
|
||||
// cloudResolverFeature is the plugin metadata feature that declares support for
|
||||
// the __resolve-context contract.
|
||||
const cloudResolverFeature = "cloud-context-resolver"
|
||||
|
||||
const cloudPluginHelp = "to use --cloud, install the plugin. See https://docs.docker.com/go/cloud-flag/"
|
||||
|
||||
func cloudProvider(dockerCli config.Provider, rootCmd *cobra.Command) (*pluginmanager.Plugin, error) {
|
||||
provider := dockerCli.ConfigFile().Features["cloud"]
|
||||
if provider == "" {
|
||||
provider = "offload"
|
||||
}
|
||||
var help string
|
||||
if provider == "offload" {
|
||||
help = "\n\n" + cloudPluginHelp
|
||||
}
|
||||
|
||||
plugin, err := pluginmanager.GetPlugin(provider, dockerCli, rootCmd)
|
||||
if err != nil {
|
||||
if provider == "offload" && errdefs.IsNotFound(err) {
|
||||
return nil, errors.New(cloudPluginHelp)
|
||||
}
|
||||
return nil, fmt.Errorf("plugin %q unavailable: %w%s", provider, err, help)
|
||||
}
|
||||
if plugin.Err != nil {
|
||||
return nil, fmt.Errorf("invalid plugin %q: %w%s", provider, plugin.Err, help)
|
||||
}
|
||||
|
||||
if supported, _ := plugin.Features[cloudResolverFeature].(bool); !supported {
|
||||
return nil, fmt.Errorf("plugin %q does not support --cloud%s", provider, help)
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// updateCloudFlagVisibility runs only for help and completion, avoiding plugin
|
||||
// discovery on ordinary invocations that do not use --cloud.
|
||||
func updateCloudFlagVisibility(dockerCli config.Provider, rootCmd *cobra.Command) {
|
||||
flag := rootCmd.Flags().Lookup("cloud")
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
_, err := cloudProvider(dockerCli, rootCmd)
|
||||
flag.Hidden = err != nil
|
||||
}
|
||||
|
||||
func resolveCloudContext(ctx context.Context, dockerCli *command.DockerCli, rootCmd *cobra.Command, name string) (string, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
plugin, err := cloudProvider(dockerCli, rootCmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, plugin.Path, "--config="+config.Dir(), plugin.Name, "__resolve-context", "--", name) // #nosec G204 -- executable validated through CLI plugin discovery
|
||||
cmd.Env = append(os.Environ(), config.EnvOverrideConfigDir+"="+config.Dir(), metadata.ReexecEnvvar+"="+os.Args[0])
|
||||
setResolverStdio(cmd, dockerCli)
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return "", ctx.Err()
|
||||
}
|
||||
return "", fmt.Errorf("cloud resolver failed: %w", err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
DockerContext string `json:"DOCKER_CONTEXT"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &response); err != nil {
|
||||
return "", fmt.Errorf("invalid cloud resolver response: %w", err)
|
||||
}
|
||||
response.DockerContext = strings.TrimSpace(response.DockerContext)
|
||||
if response.DockerContext == "" || response.DockerContext == command.DefaultContextName {
|
||||
return "", errors.New("cloud resolver must return a non-default DOCKER_CONTEXT")
|
||||
}
|
||||
|
||||
if err := validateResolvedContext(dockerCli, response.DockerContext); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return response.DockerContext, nil
|
||||
}
|
||||
|
||||
// setResolverStdio connects the resolver to the terminal when both stdin and
|
||||
// stderr are terminals, so the plugin can prompt interactively. Otherwise
|
||||
// stderr is forwarded and stdin is left unset.
|
||||
func setResolverStdio(cmd *exec.Cmd, dockerCli *command.DockerCli) {
|
||||
cmd.Stderr = dockerCli.Err()
|
||||
stdinTerminal := dockerCli.In().IsTerminal()
|
||||
stderrTerminal := dockerCli.Err().IsTerminal()
|
||||
stdin, stdinFile := dockerCli.In().File()
|
||||
stderr, stderrFile := dockerCli.Err().File()
|
||||
if runtime.GOOS == "windows" {
|
||||
// term.StdStreams can wrap console handles for terminal emulation,
|
||||
// preventing File from exposing them to the child process.
|
||||
if stdinTerminal && !stdinFile {
|
||||
stdin, stdinFile = os.Stdin, true
|
||||
}
|
||||
if stderrTerminal && !stderrFile {
|
||||
stderr, stderrFile = os.Stderr, true
|
||||
}
|
||||
}
|
||||
if stdinTerminal && stderrTerminal && stdinFile && stderrFile {
|
||||
// Pass files directly: wrapping them makes os/exec copy through pipes,
|
||||
// hiding terminal identity and potentially consuming the command's input.
|
||||
cmd.Stdin = stdin
|
||||
cmd.Stderr = stderr
|
||||
}
|
||||
}
|
||||
|
||||
// validateResolvedContext checks that the context exists and has a Docker
|
||||
// endpoint host, so a missing context or endpoint cannot fall back to the
|
||||
// local engine.
|
||||
func validateResolvedContext(dockerCli *command.DockerCli, name string) error {
|
||||
meta, err := dockerCli.ContextStore().GetMetadata(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading resolved context %q: %w", name, err)
|
||||
}
|
||||
|
||||
endpoint, err := contextdocker.EndpointFromContext(meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid resolved context %q: %w", name, err)
|
||||
}
|
||||
if endpoint.Host == "" {
|
||||
return fmt.Errorf("resolved context %q has no Docker endpoint host", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cloudHelpRequest avoids provisioning for help and shell completion.
|
||||
// Parse known command flags rather than scanning argv: --help may be an option
|
||||
// value or an argument to a container, not a request for Docker help.
|
||||
// Unknown flags and missing flag values return a usage error, so a mistyped
|
||||
// invocation fails without provisioning or falling back to the local engine.
|
||||
// Flag values are validated only by the command's own parse, after resolution.
|
||||
func cloudHelpRequest(rootCmd *cobra.Command, args []string) (bool, error) {
|
||||
help, _ := rootCmd.PersistentFlags().GetBool("help")
|
||||
version, _ := rootCmd.Flags().GetBool("version")
|
||||
if help || version || len(args) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "help", "completion", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
|
||||
return true, nil
|
||||
}
|
||||
|
||||
cmd, remaining, err := rootCmd.Find(args)
|
||||
if err != nil || cmd == rootCmd || pluginmanager.IsPluginCommand(cmd) {
|
||||
// Plugin flags are opaque. Only recognize help immediately after the
|
||||
// plugin name; deeper help is available through "docker help PLUGIN".
|
||||
return len(args) > 1 && isHelpFlag(args[1]), nil
|
||||
}
|
||||
|
||||
// "build", "bake", "builder", and "image build" are builtin commands that
|
||||
// processAliases may still rewrite into a call to the builder plugin
|
||||
// (buildx) after cloud resolution runs, so their flag set here is only the
|
||||
// legacy builder's and does not reflect what will actually parse the
|
||||
// remaining args. Treat them like a plugin command above: only recognize
|
||||
// help immediately after the resolved command path.
|
||||
if _, _, _, forwarded := forwardBuilder(builderDefaultPlugin, args, args); forwarded {
|
||||
return len(remaining) > 0 && isHelpFlag(remaining[0]), nil
|
||||
}
|
||||
|
||||
cmd.InitDefaultHelpFlag()
|
||||
flags := cmd.Flags()
|
||||
flags.AddFlagSet(cmd.PersistentFlags())
|
||||
flags.AddFlagSet(cmd.InheritedFlags())
|
||||
|
||||
err = flags.ParseAll(remaining, func(flag *pflag.Flag, value string) error {
|
||||
if flag.Name == "help" {
|
||||
var err error
|
||||
if help, err = strconv.ParseBool(value); err != nil {
|
||||
return fmt.Errorf("invalid argument %q for \"--help\" flag: %w", value, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
// Format the usage error directly. The root command's FlagErrorFunc
|
||||
// first checks whether the daemon supports the command, which would
|
||||
// connect to the engine selected before --cloud is resolved.
|
||||
return false, cli.FlagErrorFunc(cmd, err)
|
||||
}
|
||||
|
||||
return help, nil
|
||||
}
|
||||
|
||||
// isHelpFlag reports whether arg requests help, in any of the forms pflag
|
||||
// accepts for a boolean flag.
|
||||
func isHelpFlag(arg string) bool {
|
||||
return arg == "--help" || arg == "-h" || arg == "--help=true"
|
||||
}
|
||||
|
||||
// cloudPluginArgs only rewrites the global prefix, leaving subcommand arguments
|
||||
// untouched. Parsing also distinguishes --cloud from another flag's value.
|
||||
func cloudPluginArgs(cmd *cobra.Command, osArgs []string, contextName string) ([]string, error) {
|
||||
flags := pflag.NewFlagSet(cmd.Name(), pflag.ContinueOnError)
|
||||
flags.SetInterspersed(false)
|
||||
flags.AddFlagSet(cmd.Flags())
|
||||
flags.AddFlagSet(cmd.PersistentFlags())
|
||||
|
||||
result := []string{osArgs[0]}
|
||||
if contextName != "" {
|
||||
result = append(result, "--context="+contextName)
|
||||
}
|
||||
|
||||
if err := flags.ParseAll(osArgs[1:], func(flag *pflag.Flag, value string) error {
|
||||
if flag.Name != "cloud" {
|
||||
result = append(result, "--"+flag.Name+"="+value)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return osArgs, err
|
||||
}
|
||||
|
||||
if flags.ArgsLenAtDash() >= 0 {
|
||||
result = append(result, "--")
|
||||
}
|
||||
|
||||
return append(result, flags.Args()...), nil
|
||||
}
|
||||
@@ -1,682 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
contextdocker "github.com/docker/cli/cli/context/docker"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestCloudFlagHidden(t *testing.T) {
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
|
||||
tcmd := newDockerCommand(dockerCli)
|
||||
tcmd.SetArgs([]string{"--cloud=team", "--help"})
|
||||
cmd, _, err := tcmd.HandleGlobalFlags()
|
||||
assert.NilError(t, err)
|
||||
|
||||
name, err := cmd.Flags().GetString("cloud")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, name, "team")
|
||||
assert.Assert(t, cmd.Flags().Lookup("cloud").Hidden)
|
||||
assert.Assert(t, !strings.Contains(cmd.UsageString(), "--cloud"))
|
||||
}
|
||||
|
||||
func TestCloudHelpRequest(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
help bool
|
||||
}{
|
||||
{name: "no command", help: true},
|
||||
{name: "global help", args: []string{"--help"}, help: true},
|
||||
{name: "global version", args: []string{"--version"}, help: true},
|
||||
{name: "help command", args: []string{"help", "run"}, help: true},
|
||||
{name: "completion request", args: []string{"__complete", "run", ""}, help: true},
|
||||
{name: "completion script", args: []string{"completion", "fish"}, help: true},
|
||||
{name: "builtin help", args: []string{"run", "--help"}, help: true},
|
||||
{name: "boolean help value", args: []string{"run", "--help=1"}, help: true},
|
||||
{name: "nested help", args: []string{"container", "run", "--help"}, help: true},
|
||||
{name: "plugin help", args: []string{"compose", "--help"}, help: true},
|
||||
{name: "command execution", args: []string{"ps"}},
|
||||
{name: "container help", args: []string{"run", "alpine", "--help"}},
|
||||
{name: "flag value", args: []string{"run", "--name", "--help", "alpine"}},
|
||||
{name: "help disabled", args: []string{"run", "--help=false", "alpine"}},
|
||||
{name: "build help", args: []string{"build", "--help"}, help: true},
|
||||
{name: "build buildkit-only flag", args: []string{"build", "--secret", "id=s,src=./s", "."}},
|
||||
{name: "image build buildkit-only flag", args: []string{"image", "build", "--push", "."}},
|
||||
{name: "builder build buildkit-only flag", args: []string{"builder", "build", "--ssh", "default", "."}},
|
||||
{name: "bake help", args: []string{"bake", "--help"}, help: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
tcmd := newDockerCommand(dockerCli)
|
||||
tcmd.SetArgs(append([]string{"--cloud"}, tc.args...))
|
||||
cmd, args, err := tcmd.HandleGlobalFlags()
|
||||
assert.NilError(t, err)
|
||||
|
||||
help, err := cloudHelpRequest(cmd, args)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, help, tc.help)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Flag errors are reported before cloud resolution, so they must not run the
|
||||
// command's daemon feature checks against the engine selected before it.
|
||||
func TestCloudFlagErrorSkipsDaemon(t *testing.T) {
|
||||
originalConfig := config.Dir()
|
||||
t.Cleanup(func() { config.SetDir(originalConfig) })
|
||||
config.SetDir(t.TempDir())
|
||||
|
||||
var pings atomic.Int32
|
||||
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
pings.Add(1)
|
||||
w.Header().Set("Docker-Experimental", "false")
|
||||
}))
|
||||
defer daemon.Close()
|
||||
t.Setenv("DOCKER_HOST", "tcp://"+daemon.Listener.Addr().String())
|
||||
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
tcmd := newDockerCommand(dockerCli)
|
||||
tcmd.SetArgs([]string{"--cloud", "checkpoint", "ls", "--unknown"})
|
||||
cmd, args, err := tcmd.HandleGlobalFlags()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, tcmd.Initialize())
|
||||
|
||||
_, err = cloudHelpRequest(cmd, args)
|
||||
assert.Check(t, is.ErrorContains(err, "unknown flag: --unknown"))
|
||||
assert.Check(t, is.Equal(pings.Load(), int32(0)))
|
||||
}
|
||||
|
||||
func TestCloudPluginArgs(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
context string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "global flag only",
|
||||
args: []string{"docker", "--cloud=foo", "compose", "up", "--cloud=child"},
|
||||
context: "cloud-foo",
|
||||
want: []string{"docker", "--context=cloud-foo", "compose", "up", "--cloud=child"},
|
||||
},
|
||||
{
|
||||
name: "flag value is not cloud flag",
|
||||
args: []string{"docker", "--config", "--cloud", "--cloud", "-D", "compose", "up"},
|
||||
context: "cloud-default",
|
||||
want: []string{"docker", "--context=cloud-default", "--config=--cloud", "--debug=true", "compose", "up"},
|
||||
},
|
||||
{
|
||||
name: "delimiter preserved",
|
||||
args: []string{"docker", "--cloud=foo", "--", "compose", "--", "--cloud"},
|
||||
context: "cloud-foo",
|
||||
want: []string{"docker", "--context=cloud-foo", "--", "compose", "--", "--cloud"},
|
||||
},
|
||||
{
|
||||
name: "repeated cloud flags",
|
||||
args: []string{"docker", "--cloud=foo", "--cloud=bar", "compose", "up"},
|
||||
context: "cloud-bar",
|
||||
want: []string{"docker", "--context=cloud-bar", "compose", "up"},
|
||||
},
|
||||
{
|
||||
name: "help without resolution",
|
||||
args: []string{"docker", "--cloud", "compose", "--help"},
|
||||
want: []string{"docker", "compose", "--help"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
tcmd := newDockerCommand(dockerCli)
|
||||
tcmd.SetArgs(tc.args[1:])
|
||||
cmd, _, err := tcmd.HandleGlobalFlags()
|
||||
assert.NilError(t, err)
|
||||
|
||||
got, err := cloudPluginArgs(cmd, tc.args, tc.context)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, tc.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise the real startup and subprocess boundary without a daemon or cloud
|
||||
// service, including failures that must never dispatch the requested command.
|
||||
func TestCloudResolution(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fixture plugins use shell scripts")
|
||||
}
|
||||
|
||||
executable, err := os.Executable()
|
||||
assert.NilError(t, err)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
response string
|
||||
exit int
|
||||
wantName string
|
||||
wantErr string
|
||||
plugin bool
|
||||
skip bool
|
||||
provider string
|
||||
noProvider bool
|
||||
legacyProvider bool
|
||||
invalidProvider bool
|
||||
wantHelp bool
|
||||
command []string
|
||||
visible bool
|
||||
}{
|
||||
{
|
||||
name: "default target",
|
||||
args: []string{"--cloud"},
|
||||
wantName: "default",
|
||||
},
|
||||
{
|
||||
name: "empty target",
|
||||
args: []string{"--cloud="},
|
||||
wantName: "default",
|
||||
},
|
||||
{
|
||||
name: "named target",
|
||||
args: []string{"--cloud=team"},
|
||||
wantName: "team",
|
||||
},
|
||||
{
|
||||
name: "plugin dispatch",
|
||||
args: []string{"--cloud=team"},
|
||||
wantName: "team",
|
||||
plugin: true,
|
||||
},
|
||||
{
|
||||
name: "nonzero exit",
|
||||
args: []string{"--cloud"},
|
||||
exit: 1,
|
||||
wantErr: "cloud resolver failed",
|
||||
},
|
||||
{
|
||||
name: "malformed JSON",
|
||||
args: []string{"--cloud"},
|
||||
response: "not JSON",
|
||||
wantErr: "invalid cloud resolver response",
|
||||
},
|
||||
{
|
||||
name: "multiple responses",
|
||||
args: []string{"--cloud"},
|
||||
response: `{} {}`,
|
||||
wantErr: "invalid cloud resolver response",
|
||||
},
|
||||
{
|
||||
name: "missing field",
|
||||
args: []string{"--cloud"},
|
||||
response: `{}`,
|
||||
wantErr: "must return a non-default DOCKER_CONTEXT",
|
||||
},
|
||||
{
|
||||
name: "local context",
|
||||
args: []string{"--cloud"},
|
||||
response: `{"DOCKER_CONTEXT":"default"}`,
|
||||
wantErr: "must return a non-default DOCKER_CONTEXT",
|
||||
},
|
||||
{
|
||||
name: "missing context",
|
||||
args: []string{"--cloud"},
|
||||
response: `{"DOCKER_CONTEXT":"missing"}`,
|
||||
wantErr: `loading resolved context "missing"`,
|
||||
},
|
||||
{
|
||||
name: "missing endpoint host",
|
||||
args: []string{"--cloud"},
|
||||
response: `{"DOCKER_CONTEXT":"empty-host"}`,
|
||||
wantErr: "has no Docker endpoint host",
|
||||
},
|
||||
{
|
||||
name: "explicit context conflict",
|
||||
args: []string{"--cloud", "--context=other"},
|
||||
wantErr: "conflicting options",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "explicit host conflict",
|
||||
args: []string{"--cloud", "--host=tcp://localhost:2375"},
|
||||
wantErr: "conflicting options",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "invalid flag before help",
|
||||
args: []string{"--cloud"},
|
||||
command: []string{"run", "--unknown", "--help"},
|
||||
wantErr: "unknown flag: --unknown",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "default provider",
|
||||
args: []string{"--cloud"},
|
||||
noProvider: true,
|
||||
wantName: "default",
|
||||
},
|
||||
{
|
||||
name: "legacy default provider",
|
||||
args: []string{"--cloud"},
|
||||
noProvider: true,
|
||||
legacyProvider: true,
|
||||
wantErr: `plugin "offload" does not support --cloud`,
|
||||
wantHelp: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "invalid default provider",
|
||||
args: []string{"--cloud"},
|
||||
noProvider: true,
|
||||
invalidProvider: true,
|
||||
wantErr: `invalid plugin "offload": plugin metadata does not define a vendor`,
|
||||
wantHelp: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "invalid configured provider",
|
||||
args: []string{"--cloud"},
|
||||
invalidProvider: true,
|
||||
wantErr: `invalid plugin "foobar": plugin metadata does not define a vendor`,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "legacy configured provider",
|
||||
args: []string{"--cloud"},
|
||||
legacyProvider: true,
|
||||
wantErr: `plugin "foobar" does not support --cloud`,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "configured provider unavailable",
|
||||
args: []string{"--cloud"},
|
||||
provider: "missing",
|
||||
wantErr: `plugin "missing" unavailable`,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "provider cannot be a path",
|
||||
args: []string{"--cloud"},
|
||||
provider: "../foobar",
|
||||
wantErr: `plugin "../foobar" unavailable`,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "no cloud option",
|
||||
noProvider: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "help",
|
||||
args: []string{"--cloud", "--help"},
|
||||
noProvider: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
args: []string{"--cloud", "--version"},
|
||||
noProvider: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "help exposes supported provider",
|
||||
command: []string{"--help"},
|
||||
noProvider: true,
|
||||
visible: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "help hides legacy provider",
|
||||
command: []string{"--help"},
|
||||
noProvider: true,
|
||||
legacyProvider: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "help hides missing provider",
|
||||
command: []string{"--help"},
|
||||
provider: "missing",
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "completion exposes supported provider",
|
||||
command: []string{"__complete", "--cl"},
|
||||
noProvider: true,
|
||||
visible: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "completion hides legacy provider",
|
||||
command: []string{"__complete", "--cl"},
|
||||
noProvider: true,
|
||||
legacyProvider: true,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "completion hides missing provider",
|
||||
command: []string{"__complete", "--cl"},
|
||||
provider: "missing",
|
||||
skip: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Explicit --cloud must override inherited endpoint selection.
|
||||
t.Setenv("DOCKER_CONTEXT", "inherited")
|
||||
t.Setenv("DOCKER_HOST", "tcp://127.0.0.1:1")
|
||||
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
||||
t.Setenv("DOCKER_CONFIG", t.TempDir())
|
||||
|
||||
configDir := t.TempDir()
|
||||
installedProvider := "offload"
|
||||
if !tc.noProvider {
|
||||
installedProvider = "foobar"
|
||||
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
|
||||
provider := cmp.Or(tc.provider, "foobar")
|
||||
cfg.Features = map[string]string{"cloud": provider}
|
||||
assert.NilError(t, cfg.Save())
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(configDir, "cli-plugins")
|
||||
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
|
||||
|
||||
response := cmp.Or(tc.response, `{"DOCKER_CONTEXT":"resolved"}`)
|
||||
|
||||
providerMetadata := metadata.Metadata{SchemaVersion: "0.1.0", Vendor: "test"}
|
||||
if tc.invalidProvider {
|
||||
providerMetadata.Vendor = ""
|
||||
}
|
||||
if !tc.legacyProvider {
|
||||
providerMetadata.Features = map[string]any{cloudResolverFeature: true}
|
||||
}
|
||||
pluginMetadata, err := json.Marshal(providerMetadata)
|
||||
assert.NilError(t, err)
|
||||
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
if [ "$1" = docker-cli-plugin-metadata ]; then
|
||||
echo '%s'
|
||||
exit 0
|
||||
fi
|
||||
printf '%%s\n' "$@" >> "$DOCKER_CONFIG/resolver-args"
|
||||
echo provisioning >&2
|
||||
printf '%%s\n' '%s'
|
||||
exit %d
|
||||
`, pluginMetadata, response, tc.exit)
|
||||
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+installedProvider), []byte(script), 0o755))
|
||||
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-cloudtest"), []byte(`#!/bin/sh
|
||||
if [ "$1" = docker-cli-plugin-metadata ]; then
|
||||
echo '{"SchemaVersion":"0.1.0","Vendor":"test"}'
|
||||
exit 0
|
||||
fi
|
||||
printf '%s\n' "$@" > "$CLOUD_TEST_ARGS"
|
||||
`), 0o755))
|
||||
|
||||
dispatchFile := filepath.Join(configDir, "dispatch-args")
|
||||
t.Setenv("CLOUD_TEST_ARGS", dispatchFile)
|
||||
|
||||
contextStore := store.New(filepath.Join(configDir, "contexts"), command.DefaultContextStoreConfig())
|
||||
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
|
||||
Name: "resolved",
|
||||
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{Host: "tcp://127.0.0.1:1"}},
|
||||
}))
|
||||
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
|
||||
Name: "empty-host",
|
||||
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{}},
|
||||
}))
|
||||
|
||||
args := append([]string{"docker", "--config=" + configDir}, tc.args...)
|
||||
switch {
|
||||
case tc.command != nil:
|
||||
args = append(args, tc.command...)
|
||||
case tc.plugin || tc.wantErr != "":
|
||||
args = append(args, "cloudtest", "up", "--cloud=child")
|
||||
default:
|
||||
args = append(args, "context", "show")
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(args)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(t.Context(), executable, "-test.run=^TestCloudCommandProcess$")
|
||||
cmd.Env = append(os.Environ(), "CLOUD_TEST_COMMAND="+string(payload))
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if tc.wantErr != "" {
|
||||
assert.Assert(t, err != nil)
|
||||
assert.Check(t, is.Contains(stderr.String(), tc.wantErr))
|
||||
assert.Equal(t, strings.Contains(stderr.String(), cloudPluginHelp), tc.wantHelp, stderr.String())
|
||||
_, statErr := os.Stat(dispatchFile)
|
||||
assert.Assert(t, os.IsNotExist(statErr))
|
||||
assert.Equal(t, stdout.String(), "")
|
||||
} else {
|
||||
assert.NilError(t, err, stderr.String())
|
||||
if tc.command != nil {
|
||||
assert.Equal(t, strings.Contains(stdout.String(), "--cloud"), tc.visible, stdout.String())
|
||||
}
|
||||
if !tc.skip {
|
||||
if tc.plugin {
|
||||
args, err := os.ReadFile(dispatchFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(args), "--context=resolved\n--config="+configDir+"\ncloudtest\nup\n--cloud=child\n")
|
||||
} else {
|
||||
assert.Equal(t, stdout.String(), "resolved\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invocation, err := os.ReadFile(filepath.Join(configDir, "resolver-args"))
|
||||
if tc.skip {
|
||||
assert.Assert(t, os.IsNotExist(err))
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Contains(stderr.String(), "provisioning"))
|
||||
name := cmp.Or(tc.wantName, "default")
|
||||
assert.Equal(t, string(invocation), "--config="+configDir+"\n"+installedProvider+"\n__resolve-context\n--\n"+name+"\n")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudResolverInput(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fixture plugins use shell scripts and pseudo-terminals")
|
||||
}
|
||||
|
||||
executable, err := os.Executable()
|
||||
assert.NilError(t, err)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
stdinType string
|
||||
terminalErr bool
|
||||
prompt bool
|
||||
}{
|
||||
{name: "interactive", stdinType: "terminal", terminalErr: true, prompt: true},
|
||||
{name: "piped stdin", stdinType: "pipe", terminalErr: true},
|
||||
{name: "redirected stdin", stdinType: "file", terminalErr: true},
|
||||
{name: "redirected stderr", stdinType: "terminal"},
|
||||
{name: "noninteractive", stdinType: "pipe"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
||||
configDir := t.TempDir()
|
||||
pluginDir := filepath.Join(configDir, "cli-plugins")
|
||||
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
|
||||
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-offload"), []byte(`#!/bin/sh
|
||||
if [ "$1" = docker-cli-plugin-metadata ]; then
|
||||
echo '{"SchemaVersion":"0.1.0","Vendor":"test","Features":{"cloud-context-resolver":true}}'
|
||||
exit 0
|
||||
fi
|
||||
if [ -t 0 ]; then
|
||||
[ -t 2 ] || exit 1
|
||||
printf 'Continue? ' >&2
|
||||
IFS= read -r reply || exit 1
|
||||
printf '%s\n' "$reply" > "$DOCKER_CONFIG/resolver-input"
|
||||
elif IFS= read -r unexpected; then
|
||||
echo 'resolver consumed command input' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo '{"DOCKER_CONTEXT":"resolved"}'
|
||||
`), 0o755))
|
||||
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-cloudtest"), []byte(`#!/bin/sh
|
||||
if [ "$1" = docker-cli-plugin-metadata ]; then
|
||||
echo '{"SchemaVersion":"0.1.0","Vendor":"test"}'
|
||||
exit 0
|
||||
fi
|
||||
IFS= read -r input || exit 1
|
||||
printf '%s\n' "$input"
|
||||
`), 0o755))
|
||||
|
||||
contextStore := store.New(filepath.Join(configDir, "contexts"), command.DefaultContextStoreConfig())
|
||||
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
|
||||
Name: "resolved",
|
||||
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{Host: "tcp://127.0.0.1:1"}},
|
||||
}))
|
||||
|
||||
terminal, tty, err := pty.Open()
|
||||
assert.NilError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = tty.Close()
|
||||
_ = terminal.Close()
|
||||
})
|
||||
|
||||
const commandInput = "input for the original command\n"
|
||||
var stdin *os.File
|
||||
switch tc.stdinType {
|
||||
case "terminal":
|
||||
stdin = tty
|
||||
input := commandInput
|
||||
if tc.prompt {
|
||||
input = "yes\n" + input
|
||||
}
|
||||
_, err = terminal.WriteString(input)
|
||||
assert.NilError(t, err)
|
||||
case "pipe":
|
||||
var writer *os.File
|
||||
stdin, writer, err = os.Pipe()
|
||||
assert.NilError(t, err)
|
||||
t.Cleanup(func() { _ = stdin.Close() })
|
||||
t.Cleanup(func() { _ = writer.Close() })
|
||||
_, err = writer.WriteString(commandInput)
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, writer.Close())
|
||||
case "file":
|
||||
inputPath := filepath.Join(configDir, "command-input")
|
||||
assert.NilError(t, os.WriteFile(inputPath, []byte(commandInput), 0o600))
|
||||
stdin, err = os.Open(inputPath)
|
||||
assert.NilError(t, err)
|
||||
t.Cleanup(func() { _ = stdin.Close() })
|
||||
}
|
||||
|
||||
payload, err := json.Marshal([]string{"docker", "--config=" + configDir, "--cloud", "cloudtest"})
|
||||
assert.NilError(t, err)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, executable, "-test.run=^TestCloudCommandProcess$")
|
||||
cmd.Env = append(os.Environ(), "CLOUD_TEST_COMMAND="+string(payload))
|
||||
cmd.Stdin = stdin
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
cmd.WaitDelay = time.Second
|
||||
if tc.terminalErr {
|
||||
cmd.Stderr = tty
|
||||
}
|
||||
assert.NilError(t, cmd.Run(), stderr.String())
|
||||
assert.Equal(t, stdout.String(), commandInput)
|
||||
|
||||
reply, err := os.ReadFile(filepath.Join(configDir, "resolver-input"))
|
||||
if tc.prompt {
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(reply), "yes\n")
|
||||
} else {
|
||||
assert.Assert(t, os.IsNotExist(err))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Run startup in a separate process because it installs process-wide signal
|
||||
// handlers that intentionally outlive an individual command.
|
||||
func TestCloudCommandProcess(t *testing.T) {
|
||||
payload := os.Getenv("CLOUD_TEST_COMMAND")
|
||||
if payload == "" {
|
||||
return
|
||||
}
|
||||
|
||||
assert.NilError(t, json.Unmarshal([]byte(payload), &os.Args))
|
||||
|
||||
err := dockerMain(context.Background())
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
if err == nil {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
os.Exit(getExitCode(err))
|
||||
}
|
||||
|
||||
func TestCloudResolutionCancellation(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fixture plugin uses a shell script")
|
||||
}
|
||||
|
||||
originalConfig := config.Dir()
|
||||
t.Cleanup(func() { config.SetDir(originalConfig) })
|
||||
config.SetDir(t.TempDir())
|
||||
|
||||
cfg := configfile.New(filepath.Join(config.Dir(), config.ConfigFileName))
|
||||
cfg.Features = map[string]string{"cloud": "foobar"}
|
||||
assert.NilError(t, cfg.Save())
|
||||
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
|
||||
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-foobar"), []byte(`#!/bin/sh
|
||||
if [ "$1" = docker-cli-plugin-metadata ]; then
|
||||
echo '{"SchemaVersion":"0.1.0","Vendor":"test","Features":{"cloud-context-resolver":true}}'
|
||||
exit 0
|
||||
fi
|
||||
exec sleep 30
|
||||
`), 0o755))
|
||||
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
tcmd := newDockerCommand(dockerCli)
|
||||
tcmd.SetArgs(strings.Fields("--cloud context show"))
|
||||
cmd, _, err := tcmd.HandleGlobalFlags()
|
||||
assert.NilError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = resolveCloudContext(ctx, dockerCli, cmd, "default")
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
}
|
||||
+4
-24
@@ -178,9 +178,6 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
|
||||
_ = cmd.RegisterFlagCompletionFunc("log-level", completeLogLevels)
|
||||
|
||||
cmd.Flags().BoolP("version", "v", false, "Print version information and quit")
|
||||
cmd.Flags().String("cloud", "", "Use a cloud context, provisioning it if necessary (optionally --cloud=NAME)")
|
||||
cmd.Flags().Lookup("cloud").NoOptDefVal = "default"
|
||||
cmd.Flags().Lookup("cloud").Hidden = true
|
||||
setFlagErrorFunc(dockerCli, cmd)
|
||||
|
||||
setupHelpCommand(dockerCli, cmd, helpCmd)
|
||||
@@ -253,7 +250,6 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) {
|
||||
ccmd.Println(err)
|
||||
return
|
||||
}
|
||||
updateCloudFlagVisibility(dockerCli, ccmd.Root())
|
||||
|
||||
if len(args) >= 1 {
|
||||
err := tryRunPluginHelp(dockerCli, ccmd, args)
|
||||
@@ -512,18 +508,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tcmd.Initialize(
|
||||
command.WithEnableGlobalMeterProvider(),
|
||||
command.WithEnableGlobalTracerProvider(),
|
||||
command.WithContextResolver(func(cli *command.DockerCli) (string, error) {
|
||||
var err error
|
||||
os.Args, err = processCloud(ctx, cli, cmd, args, os.Args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cmd.Flags().GetString("context")
|
||||
}),
|
||||
); err != nil {
|
||||
if err := tcmd.Initialize(command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -553,7 +538,6 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
updateCloudFlagVisibility(dockerCli, cmd)
|
||||
}
|
||||
|
||||
var subCommand *cobra.Command
|
||||
@@ -571,15 +555,11 @@ 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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.10
|
||||
ARG GO_VERSION=1.26.8
|
||||
|
||||
# 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
|
||||
@@ -10,7 +10,7 @@ ARG ALPINE_VERSION=3.23
|
||||
# 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.38.0
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.10
|
||||
ARG GO_VERSION=1.26.8
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.10
|
||||
ARG GO_VERSION=1.26.8
|
||||
|
||||
# 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
|
||||
|
||||
@@ -31,7 +31,6 @@ services:
|
||||
dockerfile: engine/Dockerfile
|
||||
args:
|
||||
ENGINE_VERSION: ${ENGINE_VERSION:-29}
|
||||
ENGINE_SOURCE: ${ENGINE_SOURCE:-dind}
|
||||
privileged: true
|
||||
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
|
||||
environment:
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestRunWithCgroupNamespace(t *testing.T) {
|
||||
|
||||
func TestRunUmask(t *testing.T) {
|
||||
environment.SkipIfDaemonNotLinux(t)
|
||||
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.56"))
|
||||
t.Skip("FIXME: un-skip once e2e tests v29.8.0")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
+1
-9
@@ -3,16 +3,8 @@
|
||||
# ENGINE_VERSION is the version of the (docker-in-docker) Docker Engine to
|
||||
# test against.
|
||||
ARG ENGINE_VERSION=29
|
||||
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master).
|
||||
ARG ENGINE_SOURCE=dind
|
||||
|
||||
FROM docker:${ENGINE_VERSION}-dind AS engine-dind
|
||||
|
||||
# moby/moby-bin only has the daemon binaries.
|
||||
FROM docker:dind AS engine-moby-bin
|
||||
COPY --link --from=moby/moby-bin:master / /usr/local/bin/
|
||||
|
||||
FROM engine-${ENGINE_SOURCE}
|
||||
FROM docker:${ENGINE_VERSION}-dind
|
||||
|
||||
# the openssh-client update is needed for security reasons when using docker:23.0-dind, currently maintained as an lts by mirantis
|
||||
RUN apk --no-cache add openssl openssh-client openssh-server shadow && \
|
||||
|
||||
Vendored
+1
-10
@@ -1,14 +1,5 @@
|
||||
ARG ENGINE_VERSION
|
||||
# ENGINE_SOURCE is "dind" or "moby-bin" (moby/moby-bin:master).
|
||||
ARG ENGINE_SOURCE=dind
|
||||
|
||||
FROM docker:${ENGINE_VERSION}-dind AS engine-dind
|
||||
|
||||
# moby/moby-bin only has the daemon binaries.
|
||||
FROM docker:dind AS engine-moby-bin
|
||||
COPY --link --from=moby/moby-bin:master / /usr/local/bin/
|
||||
|
||||
FROM engine-${ENGINE_SOURCE}
|
||||
FROM docker:${ENGINE_VERSION}-dind
|
||||
|
||||
# Trust the tlsregistry CA so dockerd connects without --insecure-registry.
|
||||
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
|
||||
|
||||
@@ -42,11 +42,6 @@ setup() {
|
||||
sh e2e/testdata/registry/certs/gen-certs.sh
|
||||
fi
|
||||
|
||||
# ENGINE_VERSION=master tests against moby/moby-bin:master.
|
||||
if [ "${ENGINE_VERSION:-}" = "master" ]; then
|
||||
export ENGINE_SOURCE=moby-bin
|
||||
fi
|
||||
|
||||
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose up --build -d >&2
|
||||
|
||||
# Ensure supporting services exist before running tests. If one fails to start,
|
||||
|
||||
+13
-13
@@ -15,28 +15,28 @@ require (
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/containerd/errdefs/pkg v0.3.0
|
||||
github.com/containerd/log v0.2.0
|
||||
github.com/containerd/log v0.1.0
|
||||
github.com/containerd/platforms v1.0.0-rc.5
|
||||
github.com/creack/pty v1.1.24
|
||||
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/go-connections v0.8.2
|
||||
github.com/docker/go-connections v0.8.1
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/fvbommel/sortorder v1.2.1
|
||||
github.com/go-jose/go-jose/v4 v4.1.7
|
||||
github.com/fvbommel/sortorder v1.2.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.5
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-runewidth v0.0.31
|
||||
github.com/mattn/go-runewidth v0.0.29
|
||||
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.0
|
||||
github.com/moby/moby/client v0.6.0
|
||||
github.com/moby/patternmatcher v0.6.1
|
||||
github.com/moby/swarmkit/v2 v2.1.3
|
||||
github.com/moby/swarmkit/v2 v2.1.2
|
||||
github.com/moby/sys/atomicwriter v0.1.0
|
||||
github.com/moby/sys/capability v0.4.0
|
||||
github.com/moby/sys/sequential v0.7.0
|
||||
@@ -60,7 +60,7 @@ require (
|
||||
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.6
|
||||
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
|
||||
@@ -70,8 +70,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.3 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // 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.2 // indirect
|
||||
github.com/klauspost/compress v1.19.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
|
||||
@@ -102,7 +102,7 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.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/net v0.59.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
|
||||
|
||||
+27
-26
@@ -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 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
|
||||
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
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=
|
||||
@@ -18,8 +18,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc=
|
||||
github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU=
|
||||
github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -37,8 +37,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
|
||||
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/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
|
||||
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0=
|
||||
github.com/docker/go-events v0.1.0/go.mod h1:jwVwMgySJX4PiukIIRrind1wPGU8QhPa8aTEPkreUj4=
|
||||
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
|
||||
@@ -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.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/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/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,24 +78,24 @@ 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.2 h1:v5tiZvm1KGLyg5d5oXdXe3lsSPrbEUiKoXGia//I7Qo=
|
||||
github.com/klauspost/compress v1.20.2/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
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=
|
||||
github.com/mattn/go-runewidth v0.0.31/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mattn/go-runewidth v0.0.29 h1:3oGF3R/S2N9DQ3ptftzVIvg2eicmojCzlwBEmqEPDfQ=
|
||||
github.com/mattn/go-runewidth v0.0.29/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/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.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
|
||||
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
|
||||
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
|
||||
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.3 h1:UcwtwkIRjr75qSgUWCTIuzKUzAXlidxsiqVMnyxSF1w=
|
||||
github.com/moby/swarmkit/v2 v2.1.3/go.mod h1:KhkxT93Hm4wHoNUPJ4JRD384pS7OODFGzJWoDRj9bLE=
|
||||
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/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=
|
||||
@@ -180,8 +180,8 @@ go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT7
|
||||
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.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM=
|
||||
go.yaml.in/yaml/v3 v3.0.6/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
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=
|
||||
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=
|
||||
@@ -193,8 +193,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
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.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
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=
|
||||
@@ -203,6 +203,7 @@ golang.org/x/sync v0.23.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.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ func (csiState csiEntryState) Handle(b byte) (s state, e error) {
|
||||
|
||||
func (csiState csiEntryState) Transition(s state) error {
|
||||
csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
|
||||
_ = csiState.baseState.Transition(s)
|
||||
csiState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case csiState.parser.ground:
|
||||
@@ -34,9 +34,9 @@ func (csiState csiEntryState) Transition(s state) error {
|
||||
case csiState.parser.csiParam:
|
||||
switch {
|
||||
case sliceContains(csiParams, csiState.parser.context.currentChar):
|
||||
_ = csiState.parser.collectParam()
|
||||
csiState.parser.collectParam()
|
||||
case sliceContains(intermeds, csiState.parser.context.currentChar):
|
||||
_ = csiState.parser.collectInter()
|
||||
csiState.parser.collectInter()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -16,7 +16,8 @@ func (csiState csiParamState) Handle(b byte) (s state, e error) {
|
||||
case sliceContains(alphabetics, b):
|
||||
return csiState.parser.ground, nil
|
||||
case sliceContains(csiCollectables, b):
|
||||
return csiState, csiState.parser.collectParam()
|
||||
csiState.parser.collectParam()
|
||||
return csiState, nil
|
||||
case sliceContains(executors, b):
|
||||
return csiState, csiState.parser.execute()
|
||||
}
|
||||
@@ -26,7 +27,7 @@ func (csiState csiParamState) Handle(b byte) (s state, e error) {
|
||||
|
||||
func (csiState csiParamState) Transition(s state) error {
|
||||
csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
|
||||
_ = csiState.baseState.Transition(s)
|
||||
csiState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case csiState.parser.ground:
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
|
||||
|
||||
func (escState escapeIntermediateState) Transition(s state) error {
|
||||
escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
|
||||
_ = escState.baseState.Transition(s)
|
||||
escState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case escState.parser.ground:
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ func (escState escapeState) Handle(b byte) (s state, e error) {
|
||||
|
||||
func (escState escapeState) Transition(s state) error {
|
||||
escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name())
|
||||
_ = escState.baseState.Transition(s)
|
||||
escState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case escState.parser.ground:
|
||||
|
||||
+8
-5
@@ -113,13 +113,16 @@ func (ap *AnsiParser) handle(b byte) error {
|
||||
|
||||
if newState == nil {
|
||||
ap.logf("WARNING: newState is nil")
|
||||
return errors.New("new state of 'nil' is invalid")
|
||||
return errors.New("New state of 'nil' is invalid.")
|
||||
}
|
||||
|
||||
if newState == ap.currState {
|
||||
return nil
|
||||
if newState != ap.currState {
|
||||
if err := ap.changeState(newState); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return ap.changeState(newState)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) changeState(newState state) error {
|
||||
@@ -133,7 +136,7 @@ func (ap *AnsiParser) changeState(newState state) error {
|
||||
|
||||
// Perform transition action
|
||||
if err := ap.currState.Transition(newState); err != nil {
|
||||
ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name(), err)
|
||||
ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -4,9 +4,9 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func parseParams(bytes []byte) []string {
|
||||
var paramBuff []byte
|
||||
var params []string
|
||||
func parseParams(bytes []byte) ([]string, error) {
|
||||
paramBuff := make([]byte, 0, 0)
|
||||
params := []string{}
|
||||
|
||||
for _, v := range bytes {
|
||||
if v == ';' {
|
||||
@@ -14,7 +14,7 @@ func parseParams(bytes []byte) []string {
|
||||
// Completed parameter, append it to the list
|
||||
s := string(paramBuff)
|
||||
params = append(params, s)
|
||||
paramBuff = paramBuff[:0]
|
||||
paramBuff = make([]byte, 0, 0)
|
||||
}
|
||||
} else {
|
||||
paramBuff = append(paramBuff, v)
|
||||
@@ -27,7 +27,11 @@ func parseParams(bytes []byte) []string {
|
||||
params = append(params, s)
|
||||
}
|
||||
|
||||
return params
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func parseCmd(context ansiContext) (string, error) {
|
||||
return string(context.currentChar), nil
|
||||
}
|
||||
|
||||
func getInt(params []string, dflt int) int {
|
||||
|
||||
+9
-7
@@ -15,9 +15,10 @@ func (ap *AnsiParser) collectInter() error {
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) escDispatch() error {
|
||||
cmd := string(ap.context.currentChar)
|
||||
cmd, _ := parseCmd(*ap.context)
|
||||
intermeds := ap.context.interBuffer
|
||||
ap.logf("escDispatch currentChar: %#x", ap.context.currentChar)
|
||||
ap.logf("escDispatch: %s(%q)", cmd, string(ap.context.interBuffer))
|
||||
ap.logf("escDispatch: %v(%v)", cmd, intermeds)
|
||||
|
||||
switch cmd {
|
||||
case "D": // IND
|
||||
@@ -30,14 +31,14 @@ func (ap *AnsiParser) escDispatch() error {
|
||||
return err
|
||||
case "M": // RI
|
||||
return ap.eventHandler.RI()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) csiDispatch() error {
|
||||
cmd := string(ap.context.currentChar)
|
||||
params := parseParams(ap.context.paramBuffer)
|
||||
cmd, _ := parseCmd(*ap.context)
|
||||
params, _ := parseParams(ap.context.paramBuffer)
|
||||
ap.logf("Parsed params: %v with length: %d", params, len(params))
|
||||
|
||||
ap.logf("csiDispatch: %v(%v)", cmd, params)
|
||||
@@ -108,8 +109,9 @@ func (ap *AnsiParser) print() error {
|
||||
return ap.eventHandler.Print(ap.context.currentChar)
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) clear() {
|
||||
func (ap *AnsiParser) clear() error {
|
||||
ap.context = &ansiContext{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) execute() error {
|
||||
|
||||
+2
@@ -1,5 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
type stateID int
|
||||
|
||||
type state interface {
|
||||
Enter() error
|
||||
Exit() error
|
||||
|
||||
+10
@@ -1,5 +1,9 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func sliceContains(bytes []byte, b byte) bool {
|
||||
for _, v := range bytes {
|
||||
if v == b {
|
||||
@@ -9,3 +13,9 @@ func sliceContains(bytes []byte, b byte) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertBytesToInteger(bytes []byte) int {
|
||||
s := string(bytes)
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
}
|
||||
|
||||
+98
-1
@@ -5,9 +5,12 @@ package winterm
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"github.com/Azure/go-ansiterm"
|
||||
windows "golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Windows keyboard constants
|
||||
@@ -52,6 +55,100 @@ const (
|
||||
ENHANCED_KEY = 0x0100
|
||||
)
|
||||
|
||||
type ansiCommand struct {
|
||||
CommandBytes []byte
|
||||
Command string
|
||||
Parameters []string
|
||||
IsSpecial bool
|
||||
}
|
||||
|
||||
func newAnsiCommand(command []byte) *ansiCommand {
|
||||
|
||||
if isCharacterSelectionCmdChar(command[1]) {
|
||||
// Is Character Set Selection commands
|
||||
return &ansiCommand{
|
||||
CommandBytes: command,
|
||||
Command: string(command),
|
||||
IsSpecial: true,
|
||||
}
|
||||
}
|
||||
|
||||
// last char is command character
|
||||
lastCharIndex := len(command) - 1
|
||||
|
||||
ac := &ansiCommand{
|
||||
CommandBytes: command,
|
||||
Command: string(command[lastCharIndex]),
|
||||
IsSpecial: false,
|
||||
}
|
||||
|
||||
// more than a single escape
|
||||
if lastCharIndex != 0 {
|
||||
start := 1
|
||||
// skip if double char escape sequence
|
||||
if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY {
|
||||
start++
|
||||
}
|
||||
// convert this to GetNextParam method
|
||||
ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP)
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 {
|
||||
if index < 0 || index >= len(ac.Parameters) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
param, err := strconv.ParseInt(ac.Parameters[index], 10, 16)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return int16(param)
|
||||
}
|
||||
|
||||
func (ac *ansiCommand) String() string {
|
||||
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
|
||||
bytesToHex(ac.CommandBytes),
|
||||
ac.Command,
|
||||
strings.Join(ac.Parameters, "\",\""))
|
||||
}
|
||||
|
||||
// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
|
||||
// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
|
||||
func isAnsiCommandChar(b byte) bool {
|
||||
switch {
|
||||
case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY:
|
||||
return true
|
||||
case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM:
|
||||
// non-CSI escape sequence terminator
|
||||
return true
|
||||
case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL:
|
||||
// String escape sequence terminator
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isXtermOscSequence(command []byte, current byte) bool {
|
||||
return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL)
|
||||
}
|
||||
|
||||
func isCharacterSelectionCmdChar(b byte) bool {
|
||||
return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3)
|
||||
}
|
||||
|
||||
// bytesToHex converts a slice of bytes to a human-readable string.
|
||||
func bytesToHex(b []byte) string {
|
||||
hex := make([]string, len(b))
|
||||
for i, ch := range b {
|
||||
hex[i] = fmt.Sprintf("%X", ch)
|
||||
}
|
||||
return strings.Join(hex, "")
|
||||
}
|
||||
|
||||
// ensureInRange adjusts the passed value, if necessary, to ensure it is within
|
||||
// the passed min / max range.
|
||||
func ensureInRange(n int16, min int16, max int16) int16 {
|
||||
|
||||
+120
-146
@@ -1,15 +1,9 @@
|
||||
version: "2"
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt # files are gofmt'ed
|
||||
linters:
|
||||
enable:
|
||||
# style
|
||||
- containedctx # struct contains a context
|
||||
- dupl # duplicate code
|
||||
- dupword # duplicate words in comments
|
||||
- errname # errors are named correctly
|
||||
- intrange # integer ranges in for loop
|
||||
- errname # erorrs are named correctly
|
||||
- nolintlint # "//nolint" directives are properly explained
|
||||
- revive # golint replacement
|
||||
- unconvert # unnecessary conversions
|
||||
@@ -19,155 +13,135 @@ linters:
|
||||
- contextcheck # function uses a non-inherited context
|
||||
- errorlint # errors not wrapped for 1.13
|
||||
- exhaustive # check exhaustiveness of enum switch statements
|
||||
- gocritic # various code quality checks
|
||||
- gofmt # files are gofmt'ed
|
||||
- gosec # security
|
||||
- mirror # avoid allocations
|
||||
- nilerr # returns nil even with non-nil error
|
||||
- thelper # test helpers without t.Helper()
|
||||
- unparam # unused function params
|
||||
|
||||
exclusions:
|
||||
paths:
|
||||
- pkg/etw/sample
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
# err is very often shadowed in nested scopes
|
||||
- linters:
|
||||
- govet
|
||||
text: '^shadow: declaration of "err" shadows declaration'
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- pkg/etw/sample
|
||||
|
||||
# ignore long lines for skip autogen directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^line-length-limit: "
|
||||
source: "^//(go:generate|sys) "
|
||||
exclude-rules:
|
||||
# err is very often shadowed in nested scopes
|
||||
- linters:
|
||||
- govet
|
||||
text: '^shadow: declaration of "err" shadows declaration'
|
||||
|
||||
# ignore comment spacing for sys directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^comment-spacings: no space between comment delimiter and comment text"
|
||||
source: "//(cspell:|sys )"
|
||||
# ignore long lines for skip autogen directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^line-length-limit: "
|
||||
source: "^//(go:generate|sys) "
|
||||
|
||||
# allow unjustified ignores of error checks in defer statements
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errcheck` should provide explanation"
|
||||
source: '^\s*defer '
|
||||
#TODO: remove after upgrading to go1.18
|
||||
# ignore comment spacing for nolint and sys directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^comment-spacings: no space between comment delimiter and comment text"
|
||||
source: "//(cspell:|nolint:|sys |todo)"
|
||||
|
||||
# allow unjustified ignores of error lints for io.EOF
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errorlint` should provide explanation"
|
||||
source: '[=|!]= io.EOF'
|
||||
# not on go 1.18 yet, so no any
|
||||
- linters:
|
||||
- revive
|
||||
text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'"
|
||||
|
||||
- linters:
|
||||
- gosec
|
||||
text: "^G115: integer overflow conversion"
|
||||
# allow unjustified ignores of error checks in defer statements
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errcheck` should provide explanation"
|
||||
source: '^\s*defer '
|
||||
|
||||
# allow unjustified ignores of error lints for io.EOF
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errorlint` should provide explanation"
|
||||
source: '[=|!]= io.EOF'
|
||||
|
||||
|
||||
settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
gocritic:
|
||||
disable-all: true
|
||||
enabled-checks:
|
||||
- nilValReturn
|
||||
- sprintfQuotedString
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# struct order is often for Win32 compat
|
||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||
- fieldalignment
|
||||
nolintlint:
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
revive:
|
||||
# revive is more configurable than static check, so likely the preferred alternative to static-check
|
||||
# (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997)
|
||||
enable-all-rules:
|
||||
true
|
||||
linters-settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# struct order is often for Win32 compat
|
||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||
- fieldalignment
|
||||
nolintlint:
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
revive:
|
||||
# revive is more configurable than static check, so likely the preferred alternative to static-check
|
||||
# (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997)
|
||||
enable-all-rules:
|
||||
true
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
|
||||
rules:
|
||||
# rules with required arguments
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
# covered by "exhaustive" linter
|
||||
- name: enforce-switch-style
|
||||
disabled: true
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
# generally annoying rules
|
||||
- name: add-constant # complains about any and all strings and integers
|
||||
disabled: true
|
||||
- name: confusing-naming # we frequently use "Foo()" and "foo()" together
|
||||
disabled: true
|
||||
- name: flag-parameter # excessive, and a common idiom we use
|
||||
disabled: true
|
||||
- name: multiline-if-init # too opinionated; see https://github.com/microsoft/go-winio/pull/325#issuecomment-5822487436
|
||||
disabled: true
|
||||
- name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead
|
||||
disabled: true
|
||||
# general config
|
||||
- name: line-length-limit
|
||||
arguments:
|
||||
- 140
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- []
|
||||
- - CID
|
||||
- CRI
|
||||
- CTRD
|
||||
- DACL
|
||||
- DLL
|
||||
- DOS
|
||||
- ETW
|
||||
- FSCTL
|
||||
- GCS
|
||||
- GMSA
|
||||
- HCS
|
||||
- HV
|
||||
- IO
|
||||
- LCOW
|
||||
- LDAP
|
||||
- LPAC
|
||||
- LTSC
|
||||
- MMIO
|
||||
- NT
|
||||
- OCI
|
||||
- PMEM
|
||||
- PWSH
|
||||
- RX
|
||||
- SACl
|
||||
- SID
|
||||
- SMB
|
||||
- TX
|
||||
- VHD
|
||||
- VHDX
|
||||
- VMID
|
||||
- VPCI
|
||||
- WCOW
|
||||
- WIM
|
||||
staticcheck:
|
||||
# Enable all options, with some exceptions.
|
||||
# For defaults, see https://golangci-lint.run/usage/linters/#staticcheck
|
||||
checks:
|
||||
- all
|
||||
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
|
||||
- -ST1003 # Poorly chosen identifier; https://staticcheck.dev/docs/checks/#ST1003
|
||||
rules:
|
||||
# rules with required arguments
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
# geneally annoying rules
|
||||
- name: add-constant # complains about any and all strings and integers
|
||||
disabled: true
|
||||
- name: confusing-naming # we frequently use "Foo()" and "foo()" together
|
||||
disabled: true
|
||||
- name: flag-parameter # excessive, and a common idiom we use
|
||||
disabled: true
|
||||
- name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead
|
||||
disabled: true
|
||||
# general config
|
||||
- name: line-length-limit
|
||||
arguments:
|
||||
- 140
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- []
|
||||
- - CID
|
||||
- CRI
|
||||
- CTRD
|
||||
- DACL
|
||||
- DLL
|
||||
- DOS
|
||||
- ETW
|
||||
- FSCTL
|
||||
- GCS
|
||||
- GMSA
|
||||
- HCS
|
||||
- HV
|
||||
- IO
|
||||
- LCOW
|
||||
- LDAP
|
||||
- LPAC
|
||||
- LTSC
|
||||
- MMIO
|
||||
- NT
|
||||
- OCI
|
||||
- PMEM
|
||||
- PWSH
|
||||
- RX
|
||||
- SACl
|
||||
- SID
|
||||
- SMB
|
||||
- TX
|
||||
- VHD
|
||||
- VHDX
|
||||
- VMID
|
||||
- VPCI
|
||||
- WCOW
|
||||
- WIM
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
@@ -35,7 +36,7 @@ const (
|
||||
StreamSparseAttributes = uint32(8)
|
||||
)
|
||||
|
||||
//nolint:revive,nolintlint // var-naming: ALL_CAPS
|
||||
//nolint:revive // var-naming: ALL_CAPS
|
||||
const (
|
||||
WRITE_DAC = windows.WRITE_DAC
|
||||
WRITE_OWNER = windows.WRITE_OWNER
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.WriteString(ea.Name)
|
||||
_, err = buf.Write([]byte(ea.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+12
-34
@@ -1,11 +1,11 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -21,33 +21,16 @@ 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
|
||||
|
||||
// ErrFileClosed is returned when an operation is performed on a closed file.
|
||||
//
|
||||
// Deprecated: use [os.ErrClosed] instead.
|
||||
var ErrFileClosed = os.ErrClosed
|
||||
var (
|
||||
ErrFileClosed = errors.New("file has already been closed")
|
||||
ErrTimeout = &timeoutError{}
|
||||
)
|
||||
|
||||
// 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{}
|
||||
|
||||
@@ -161,7 +144,7 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
f.wgLock.RLock()
|
||||
if f.closing.Load() {
|
||||
f.wgLock.RUnlock()
|
||||
return nil, os.ErrClosed
|
||||
return nil, ErrFileClosed
|
||||
}
|
||||
f.wg.Add(1)
|
||||
f.wgLock.RUnlock()
|
||||
@@ -210,7 +193,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 = os.ErrClosed
|
||||
err = ErrFileClosed
|
||||
}
|
||||
} else if err != nil && f.socket {
|
||||
// err is from Win32. Query the overlapped structure to get the winsock error.
|
||||
@@ -252,17 +235,12 @@ func (f *win32File) Read(b []byte) (int, error) {
|
||||
runtime.KeepAlive(b)
|
||||
|
||||
// Handle EOF conditions.
|
||||
switch err {
|
||||
case windows.ERROR_BROKEN_PIPE: //nolint:errorlint // err is Errno
|
||||
if err == nil && n == 0 && len(b) != 0 {
|
||||
return 0, io.EOF
|
||||
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
||||
return 0, io.EOF
|
||||
case nil:
|
||||
if n == 0 && len(b) != 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
default:
|
||||
return n, err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Write writes to a file handle.
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
@@ -441,7 +442,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, os.ErrClosed) {
|
||||
if errors.Is(err, ErrFileClosed) {
|
||||
err = socket.ErrSocketClosed
|
||||
}
|
||||
return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err}
|
||||
|
||||
+1
-1
@@ -243,7 +243,7 @@ const (
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew
|
||||
func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) {
|
||||
b := stringbuffer.NewWString()
|
||||
// TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n?
|
||||
//TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n?
|
||||
for {
|
||||
n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags))
|
||||
if err != nil {
|
||||
|
||||
+6
-2
@@ -15,8 +15,12 @@ var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
@@ -26,7 +30,7 @@ func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case windows.ERROR_IO_PENDING:
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
|
||||
+6
-2
@@ -15,8 +15,12 @@ var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
@@ -26,7 +30,7 @@ func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case windows.ERROR_IO_PENDING:
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
// TODO: worth exporting and using in mkwinsyscall?
|
||||
|
||||
// MinWStringCap is the buffer size in the pool, chosen somewhat arbitrarily to accommodate
|
||||
// Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate
|
||||
// large path strings:
|
||||
// MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310.
|
||||
const MinWStringCap = 310
|
||||
@@ -16,7 +16,7 @@ const MinWStringCap = 310
|
||||
// is copied to heap and then referenced via pointer in the interface header that sync.Pool
|
||||
// stores.
|
||||
var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly
|
||||
New: func() any {
|
||||
New: func() interface{} {
|
||||
b := make([]uint16, MinWStringCap)
|
||||
return &b
|
||||
},
|
||||
|
||||
+30
-31
@@ -1,4 +1,5 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
@@ -10,7 +11,6 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -102,10 +102,12 @@ func (status ntStatus) Err() error {
|
||||
return rtlNtStatusToDosError(status)
|
||||
}
|
||||
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
//
|
||||
// Deprecated: use [net.ErrClosed] instead.
|
||||
var ErrPipeListenerClosed = net.ErrClosed
|
||||
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")
|
||||
)
|
||||
|
||||
type win32Pipe struct {
|
||||
*win32File
|
||||
@@ -142,10 +144,9 @@ 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 io.ErrClosedPipe
|
||||
return errPipeWriteClosed
|
||||
}
|
||||
err := f.win32File.Flush()
|
||||
if err != nil {
|
||||
@@ -160,10 +161,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. It returns [io.ErrClosedPipe] if the write side is closed.
|
||||
// they are used to implement CloseWrite().
|
||||
func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
|
||||
if f.writeClosed {
|
||||
return 0, io.ErrClosedPipe
|
||||
return 0, errPipeWriteClosed
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
@@ -178,15 +179,14 @@ func (f *win32MessageBytePipe) Read(b []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err := f.win32File.Read(b)
|
||||
switch err {
|
||||
case io.EOF: //nolint:errorlint // error is not wrapped.
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
// If this was the result of a zero-byte read, then
|
||||
// it is possible that the read was due to a zero-size
|
||||
// message. Since we are simulating CloseWrite with a
|
||||
// zero-byte message, ensure that all future Read() calls
|
||||
// also return EOF.
|
||||
f.readEOF = true
|
||||
case windows.ERROR_MORE_DATA:
|
||||
} else if err == windows.ERROR_MORE_DATA { //nolint:errorlint // err is Errno
|
||||
// ERROR_MORE_DATA indicates that the pipe's read mode is message mode
|
||||
// and the message still has more bytes. Treat this as a success, since
|
||||
// this package presents all named pipes as byte streams.
|
||||
@@ -316,9 +316,8 @@ type win32PipeListener struct {
|
||||
path string
|
||||
config PipeConfig
|
||||
acceptCh chan (chan acceptResponse)
|
||||
closeOnce sync.Once
|
||||
closeCh chan struct{} // closed (never sent on) to broadcast listener shutdown
|
||||
doneCh chan struct{}
|
||||
closeCh chan int
|
||||
doneCh chan int
|
||||
}
|
||||
|
||||
func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) {
|
||||
@@ -345,7 +344,7 @@ func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (wi
|
||||
// The security descriptor is only needed for the first pipe.
|
||||
if first {
|
||||
if sd != nil {
|
||||
// todo: does `sdb` need to be allocated on the heap, or can go allocate it?
|
||||
//todo: does `sdb` need to be allocated on the heap, or can go allocate it?
|
||||
l := uint32(len(sd))
|
||||
sdb, err := windows.LocalAlloc(0, l)
|
||||
if err != nil {
|
||||
@@ -446,14 +445,13 @@ func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
|
||||
p = nil
|
||||
}
|
||||
case <-l.closeCh:
|
||||
// Abort the connect request by closing the handle. Listener closure is
|
||||
// authoritative: ConnectNamedPipe may race the handle close and report a
|
||||
// connection or error (e.g. ERROR_NO_DATA) instead of ErrFileClosed, and
|
||||
// that result must not be surfaced or cause listenerRoutine to retry.
|
||||
_ = p.Close()
|
||||
// Abort the connect request by closing the handle.
|
||||
p.Close()
|
||||
p = nil
|
||||
<-ch
|
||||
err = net.ErrClosed
|
||||
err = <-ch
|
||||
if err == nil || err == ErrFileClosed { //nolint:errorlint // err is Errno
|
||||
err = ErrPipeListenerClosed
|
||||
}
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
@@ -478,7 +476,7 @@ func (l *win32PipeListener) listenerRoutine() {
|
||||
}
|
||||
}
|
||||
responseCh <- acceptResponse{p, err}
|
||||
closed = err == net.ErrClosed //nolint:errorlint // err is Errno
|
||||
closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
|
||||
}
|
||||
}
|
||||
windows.Close(l.firstHandle)
|
||||
@@ -532,8 +530,8 @@ func ListenPipe(path string, c *PipeConfig) (net.Listener, error) {
|
||||
path: path,
|
||||
config: *c,
|
||||
acceptCh: make(chan (chan acceptResponse)),
|
||||
closeCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
closeCh: make(chan int),
|
||||
doneCh: make(chan int),
|
||||
}
|
||||
go l.listenerRoutine()
|
||||
return l, nil
|
||||
@@ -570,15 +568,16 @@ func (l *win32PipeListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
return &win32Pipe{win32File: response.f, path: l.path}, nil
|
||||
case <-l.doneCh:
|
||||
return nil, net.ErrClosed
|
||||
return nil, ErrPipeListenerClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) Close() error {
|
||||
l.closeOnce.Do(func() {
|
||||
close(l.closeCh)
|
||||
})
|
||||
<-l.doneCh
|
||||
select {
|
||||
case l.closeCh <- 1:
|
||||
<-l.doneCh
|
||||
case <-l.doneCh:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//go:generate go tool stringer -type=Variant -trimprefix=Variant -linecomment
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type=Variant -trimprefix=Variant -linecomment
|
||||
|
||||
// Variant specifies which GUID variant (or "type") of the GUID. It determines
|
||||
// how the entirety of the rest of the GUID is interpreted.
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package guid
|
||||
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package guid
|
||||
|
||||
|
||||
+2
-3
@@ -20,9 +20,8 @@ const _Variant_name = "UnknownNCSRFC 4122MicrosoftFuture"
|
||||
var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33}
|
||||
|
||||
func (i Variant) String() string {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_Variant_index)-1 {
|
||||
if i >= Variant(len(_Variant_index)-1) {
|
||||
return "Variant(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Variant_name[_Variant_index[idx]:_Variant_index[idx+1]]
|
||||
return _Variant_name[_Variant_index[i]:_Variant_index[i+1]]
|
||||
}
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
|
||||
+6
-55
@@ -1,11 +1,11 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
@@ -15,9 +15,6 @@ import (
|
||||
const (
|
||||
reparseTagMountPoint = 0xA0000003
|
||||
reparseTagSymlink = 0xA000000C
|
||||
reparseTagLxSymlink = 0xA000001D // WSL/MSYS2 native symlinks
|
||||
|
||||
lxSymlinkVersion = 2 // LX symlink format version
|
||||
)
|
||||
|
||||
type reparseDataBuffer struct {
|
||||
@@ -34,7 +31,6 @@ type reparseDataBuffer struct {
|
||||
type ReparsePoint struct {
|
||||
Target string
|
||||
IsMountPoint bool
|
||||
IsLxSymlink bool // True if this is an LX symlink (WSL/MSYS2 native)
|
||||
}
|
||||
|
||||
// UnsupportedReparsePointError is returned when trying to decode a non-symlink or
|
||||
@@ -55,19 +51,14 @@ func DecodeReparsePoint(b []byte) (*ReparsePoint, error) {
|
||||
}
|
||||
|
||||
func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
|
||||
isMountPoint := false
|
||||
switch tag {
|
||||
case reparseTagMountPoint:
|
||||
return decodeWindowsReparsePointData(b, true)
|
||||
isMountPoint = true
|
||||
case reparseTagSymlink:
|
||||
return decodeWindowsReparsePointData(b, false)
|
||||
case reparseTagLxSymlink:
|
||||
return decodeLxReparsePointData(b)
|
||||
default:
|
||||
return nil, &UnsupportedReparsePointError{tag}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeWindowsReparsePointData(b []byte, isMountPoint bool) (*ReparsePoint, error) {
|
||||
nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])
|
||||
if !isMountPoint {
|
||||
nameOffset += 4
|
||||
@@ -78,56 +69,16 @@ func decodeWindowsReparsePointData(b []byte, isMountPoint bool) (*ReparsePoint,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ReparsePoint{Target: string(utf16.Decode(name)), IsMountPoint: isMountPoint, IsLxSymlink: false}, nil
|
||||
}
|
||||
|
||||
func decodeLxReparsePointData(b []byte) (*ReparsePoint, error) {
|
||||
// LX symlinks store the target as UTF-8 after a 4-byte version field
|
||||
if len(b) < 4 {
|
||||
return nil, errors.New("LX symlink buffer too short")
|
||||
}
|
||||
targetBytes := b[4:]
|
||||
for i, c := range targetBytes {
|
||||
if c == 0 {
|
||||
targetBytes = targetBytes[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
target := string(targetBytes)
|
||||
return &ReparsePoint{Target: target, IsMountPoint: false, IsLxSymlink: true}, nil
|
||||
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
|
||||
}
|
||||
|
||||
func isDriveLetter(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink,
|
||||
// mount point, or LX symlink.
|
||||
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or
|
||||
// mount point.
|
||||
func EncodeReparsePoint(rp *ReparsePoint) []byte {
|
||||
if rp == nil {
|
||||
return nil
|
||||
}
|
||||
if rp.IsLxSymlink {
|
||||
return encodeLxReparsePoint(rp)
|
||||
}
|
||||
return encodeWindowsReparsePoint(rp)
|
||||
}
|
||||
|
||||
func encodeLxReparsePoint(rp *ReparsePoint) []byte {
|
||||
// LX symlink: 4-byte version + UTF-8 target
|
||||
targetBytes := []byte(rp.Target)
|
||||
dataLength := 4 + len(targetBytes)
|
||||
|
||||
var b bytes.Buffer
|
||||
_ = binary.Write(&b, binary.LittleEndian, uint32(reparseTagLxSymlink))
|
||||
_ = binary.Write(&b, binary.LittleEndian, uint16(dataLength))
|
||||
_ = binary.Write(&b, binary.LittleEndian, uint16(0))
|
||||
_ = binary.Write(&b, binary.LittleEndian, uint32(lxSymlinkVersion))
|
||||
_, _ = b.Write(targetBytes)
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func encodeWindowsReparsePoint(rp *ReparsePoint) []byte {
|
||||
// Generate an NT path and determine if this is a relative path.
|
||||
var ntTarget string
|
||||
relative := false
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
|
||||
+6
-2
@@ -15,8 +15,12 @@ var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = windows.ERROR_IO_PENDING
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
@@ -26,7 +30,7 @@ func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case windows.ERROR_IO_PENDING:
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
+15
-35
@@ -1,50 +1,30 @@
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- copyloopvar
|
||||
- dupword
|
||||
- exportloopref # Checks for pointers to enclosing loop variables
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nolintlint
|
||||
- revive
|
||||
- staticcheck
|
||||
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
|
||||
- unconvert
|
||||
- unused
|
||||
- vet
|
||||
- dupword # Checks for duplicate words in the source code
|
||||
disable:
|
||||
- errcheck
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- api
|
||||
- cluster
|
||||
- design
|
||||
- docs
|
||||
- docs/man
|
||||
- releases
|
||||
- reports
|
||||
- test
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- api
|
||||
- cluster
|
||||
- design
|
||||
- docs
|
||||
- docs/man
|
||||
- releases
|
||||
- reports
|
||||
- test
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
skip-dirs:
|
||||
- api
|
||||
- cluster
|
||||
- design
|
||||
- docs
|
||||
- docs/man
|
||||
- releases
|
||||
- reports
|
||||
- test # e2e scripts
|
||||
|
||||
+9
-61
@@ -40,11 +40,8 @@ package log
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
lslog "github.com/sirupsen/logrus/hooks/slog"
|
||||
)
|
||||
|
||||
// G is a shorthand for [GetLogger].
|
||||
@@ -111,17 +108,10 @@ const (
|
||||
PanicLevel Level = logrus.PanicLevel
|
||||
)
|
||||
|
||||
// levelValue is a log level accepted by [SetLevel].
|
||||
type levelValue interface {
|
||||
string | Level | slog.Level | lslog.Level | lslog.SlogLevel
|
||||
}
|
||||
|
||||
// SetLevel sets the global log level.
|
||||
// SetLevel sets log level globally. It returns an error if the given
|
||||
// level is not supported.
|
||||
//
|
||||
// The level may be specified as a string, a [Level], a [slog.Level], an
|
||||
// [lslog.Level], or an [lslog.SlogLevel].
|
||||
//
|
||||
// String levels are parsed using [logrus.ParseLevel] and may be one of:
|
||||
// level can be one of:
|
||||
//
|
||||
// - "trace" ([TraceLevel])
|
||||
// - "debug" ([DebugLevel])
|
||||
@@ -130,34 +120,10 @@ type levelValue interface {
|
||||
// - "error" ([ErrorLevel])
|
||||
// - "fatal" ([FatalLevel])
|
||||
// - "panic" ([PanicLevel])
|
||||
//
|
||||
// slog levels are mapped to Logrus levels using the mapping defined by
|
||||
// [lslog.SlogLevel]. [lslog.Level] values are accepted directly as their
|
||||
// underlying Logrus level.
|
||||
//
|
||||
// SetLevel returns an error if a string level is not supported.
|
||||
func SetLevel[T levelValue](level T) error {
|
||||
var lvl Level
|
||||
|
||||
switch l := any(level).(type) {
|
||||
case string:
|
||||
var err error
|
||||
lvl, err = logrus.ParseLevel(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case Level:
|
||||
lvl = l
|
||||
|
||||
case slog.Level:
|
||||
lvl = lslog.SlogLevel(l).Level()
|
||||
|
||||
case lslog.Level:
|
||||
lvl = Level(l)
|
||||
|
||||
case lslog.SlogLevel:
|
||||
lvl = l.Level()
|
||||
func SetLevel(level string) error {
|
||||
lvl, err := logrus.ParseLevel(level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
L.Logger.SetLevel(lvl)
|
||||
@@ -183,39 +149,21 @@ const (
|
||||
|
||||
// SetFormat sets the log output format ([TextFormat] or [JSONFormat]).
|
||||
func SetFormat(format OutputFormat) error {
|
||||
if slogOut != nil {
|
||||
var handler slog.Handler
|
||||
switch format {
|
||||
case TextFormat:
|
||||
handler = slog.NewTextHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}})
|
||||
case JSONFormat:
|
||||
handler = slog.NewJSONHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}})
|
||||
default:
|
||||
return fmt.Errorf("unknown log format: %s", format)
|
||||
}
|
||||
slog.SetDefault(slog.New(handler))
|
||||
|
||||
// Keep logrus formatting and output disabled, as both are handled by slog.
|
||||
L.Logger.SetFormatter(discardFormatter{})
|
||||
L.Logger.SetOutput(io.Discard)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch format {
|
||||
case TextFormat:
|
||||
L.Logger.SetFormatter(&logrus.TextFormatter{
|
||||
TimestampFormat: RFC3339NanoFixed,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
return nil
|
||||
case JSONFormat:
|
||||
L.Logger.SetFormatter(&logrus.JSONFormatter{
|
||||
TimestampFormat: RFC3339NanoFixed,
|
||||
})
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown log format: %s", format)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithLogger returns a new context with the provided logger. Use in
|
||||
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
lslog "github.com/sirupsen/logrus/hooks/slog"
|
||||
)
|
||||
|
||||
// slogOut is used to set the slog logger when setting output format.
|
||||
var slogOut io.Writer
|
||||
|
||||
// slogOnce guards UseSlog so repeated calls do not stack up hooks or
|
||||
// reset slogOut to the discard writer installed on the first call.
|
||||
var slogOnce sync.Once
|
||||
|
||||
func UseSlog() {
|
||||
slogOnce.Do(func() {
|
||||
L.Logger.SetNoLock()
|
||||
L.Logger.AddHook(slogHook{})
|
||||
slogOut = L.Logger.Out
|
||||
|
||||
// Disable logrus formatting and output, as both are handled by slog.
|
||||
L.Logger.SetFormatter(discardFormatter{})
|
||||
L.Logger.SetOutput(io.Discard)
|
||||
})
|
||||
}
|
||||
|
||||
type slogHook struct{}
|
||||
|
||||
func (slogHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
func (slogHook) Fire(entry *logrus.Entry) error {
|
||||
return lslog.NewHook(slog.Default(), nil).Fire(entry)
|
||||
}
|
||||
|
||||
// loggerLevel exposes the current Logrus logger level as a slog.Leveler.
|
||||
type loggerLevel struct{}
|
||||
|
||||
func (loggerLevel) Level() slog.Level {
|
||||
return lslog.Level(L.Logger.GetLevel()).Level()
|
||||
}
|
||||
|
||||
type discardFormatter struct{}
|
||||
|
||||
func (discardFormatter) Format(*logrus.Entry) ([]byte, error) { return nil, nil }
|
||||
+1
-5
@@ -9,17 +9,13 @@ import (
|
||||
// NewTCPSocket creates a TCP socket listener with the specified address and
|
||||
// the specified tls configuration. If TLSConfig is set, will encapsulate the
|
||||
// TCP listener inside a TLS one.
|
||||
// If no ALPN protocols are configured, it defaults to HTTP/1.1.
|
||||
func NewTCPSocket(addr string, tlsConfig *tls.Config) (net.Listener, error) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
if len(tlsConfig.NextProtos) == 0 {
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
tlsConfig.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
tlsConfig.NextProtos = []string{"http/1.1"}
|
||||
l = tls.NewListener(l, tlsConfig)
|
||||
}
|
||||
return l, nil
|
||||
|
||||
+3
-33
@@ -408,10 +408,7 @@ func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWe
|
||||
(*obj.protected)[k] = makeRawMessage(b)
|
||||
}
|
||||
|
||||
authData, err := obj.computeAuthData()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authData := obj.computeAuthData()
|
||||
parts, err := ctx.cipher.encrypt(cek, authData, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -431,13 +428,6 @@ 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.
|
||||
@@ -495,10 +485,7 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
|
||||
tag: obj.tag,
|
||||
}
|
||||
|
||||
authData, err := obj.computeAuthData()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authData := obj.computeAuthData()
|
||||
|
||||
var plaintext []byte
|
||||
recipient := obj.recipients[0]
|
||||
@@ -508,10 +495,6 @@ 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)
|
||||
@@ -537,12 +520,6 @@ 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)
|
||||
|
||||
@@ -576,10 +553,7 @@ func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Heade
|
||||
tag: obj.tag,
|
||||
}
|
||||
|
||||
authData, err := obj.computeAuthData()
|
||||
if err != nil {
|
||||
return -1, Header{}, nil, err
|
||||
}
|
||||
authData := obj.computeAuthData()
|
||||
|
||||
index := -1
|
||||
var plaintext []byte
|
||||
@@ -598,10 +572,6 @@ 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)
|
||||
|
||||
+32
-6
@@ -30,6 +30,30 @@ 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{}
|
||||
@@ -98,8 +122,6 @@ 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
|
||||
}
|
||||
@@ -147,13 +169,13 @@ func (b *byteBuffer) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.data = decoded
|
||||
*b = *newBuffer(decoded)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *byteBuffer) base64() string {
|
||||
return base64.RawURLEncoding.EncodeToString(b.bytes())
|
||||
return base64.RawURLEncoding.EncodeToString(b.data)
|
||||
}
|
||||
|
||||
func (b *byteBuffer) bytes() []byte {
|
||||
@@ -164,8 +186,12 @@ func (b *byteBuffer) bytes() []byte {
|
||||
return b.data
|
||||
}
|
||||
|
||||
func (b *byteBuffer) bigInt() *big.Int {
|
||||
return new(big.Int).SetBytes(b.bytes())
|
||||
func (b byteBuffer) bigInt() *big.Int {
|
||||
return new(big.Int).SetBytes(b.data)
|
||||
}
|
||||
|
||||
func (b byteBuffer) toInt() int {
|
||||
return int(b.bigInt().Int64())
|
||||
}
|
||||
|
||||
func base64EncodeLen(sl []byte) int {
|
||||
|
||||
-13
@@ -671,19 +671,6 @@ 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()
|
||||
|
||||
+1
-13
@@ -1025,19 +1025,7 @@ 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.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.
|
||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||
continue
|
||||
}
|
||||
tag := sf.Tag.Get("json")
|
||||
|
||||
+25
-59
@@ -25,12 +25,6 @@ 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"`
|
||||
@@ -90,18 +84,14 @@ func (obj JSONWebEncryption) mergedHeaders(recipient *recipientInfo) rawHeader {
|
||||
}
|
||||
|
||||
// Get the additional authenticated data from a JWE object.
|
||||
func (obj JSONWebEncryption) computeAuthData() ([]byte, error) {
|
||||
func (obj JSONWebEncryption) computeAuthData() []byte {
|
||||
var protected string
|
||||
|
||||
switch {
|
||||
case obj.original != nil && obj.original.Protected != nil:
|
||||
protected = obj.original.Protected.base64()
|
||||
case obj.protected != nil:
|
||||
serializedProtected, err := json.Marshal(obj.protected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
protected = base64.RawURLEncoding.EncodeToString(serializedProtected)
|
||||
protected = base64.RawURLEncoding.EncodeToString(mustSerializeJSON((obj.protected)))
|
||||
default:
|
||||
protected = ""
|
||||
}
|
||||
@@ -112,7 +102,7 @@ func (obj JSONWebEncryption) computeAuthData() ([]byte, error) {
|
||||
output = append(output, []byte(base64.RawURLEncoding.EncodeToString(obj.aad))...)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
return output
|
||||
}
|
||||
|
||||
func containsKeyAlgorithm(haystack []KeyAlgorithm, needle KeyAlgorithm) bool {
|
||||
@@ -138,10 +128,11 @@ 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 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.
|
||||
// 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.
|
||||
func ParseEncrypted(input string,
|
||||
keyEncryptionAlgorithms []KeyAlgorithm,
|
||||
contentEncryption []ContentEncryption,
|
||||
@@ -154,13 +145,9 @@ func ParseEncrypted(input string,
|
||||
return ParseEncryptedCompact(input, keyEncryptionAlgorithms, contentEncryption)
|
||||
}
|
||||
|
||||
// ParseEncryptedJSON parses a message in JWE JSON Serialization. Messages with
|
||||
// more than [MaxRecipients] recipients are rejected.
|
||||
// ParseEncryptedJSON parses a message in JWE JSON Serialization.
|
||||
//
|
||||
// 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,
|
||||
@@ -190,9 +177,6 @@ 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,
|
||||
@@ -211,11 +195,21 @@ func (parsed *rawJSONWebEncryption) sanitized(
|
||||
}
|
||||
}
|
||||
|
||||
protected, err := parseProtectedHeader(parsed.Protected)
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("go-jose/go-jose: cannot sanitize merged headers: %v (%v)", err, mergedHeaders)
|
||||
}
|
||||
obj.protected = protected
|
||||
|
||||
if len(parsed.Recipients) == 0 {
|
||||
obj.recipients = []recipientInfo{
|
||||
@@ -270,14 +264,6 @@ 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()
|
||||
@@ -301,9 +287,6 @@ 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,
|
||||
@@ -371,10 +354,7 @@ func (obj JSONWebEncryption) CompactSerialize() (string, error) {
|
||||
return "", ErrNotSupported
|
||||
}
|
||||
|
||||
serializedProtected, err := json.Marshal(obj.protected)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
serializedProtected := mustSerializeJSON(obj.protected)
|
||||
|
||||
return base64JoinWithDots(
|
||||
serializedProtected,
|
||||
@@ -386,13 +366,7 @@ 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),
|
||||
@@ -418,16 +392,8 @@ func (obj JSONWebEncryption) FullSerialize() string {
|
||||
}
|
||||
|
||||
if obj.protected != nil {
|
||||
serializedProtected, err := json.Marshal(obj.protected)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
raw.Protected = newBuffer(serializedProtected)
|
||||
raw.Protected = newBuffer(mustSerializeJSON(obj.protected))
|
||||
}
|
||||
|
||||
out, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
return string(mustSerializeJSON(raw))
|
||||
}
|
||||
|
||||
+24
-74
@@ -30,7 +30,6 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"reflect"
|
||||
@@ -178,29 +177,13 @@ func (k JSONWebKey) MarshalJSON() ([]byte, error) {
|
||||
// UnmarshalJSON reads a key from its JSON representation.
|
||||
//
|
||||
// Returns ErrUnsupportedKeyType for unrecognized or unsupported "kty" header values.
|
||||
//
|
||||
// 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) {
|
||||
func (k *JSONWebKey) UnmarshalJSON(data []byte) (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)
|
||||
@@ -250,7 +233,7 @@ func (k *JSONWebKey) unmarshalJSON(data []byte, allowPrivate bool) (err error) {
|
||||
keyPub = key
|
||||
}
|
||||
case "oct":
|
||||
if len(certs) > 0 {
|
||||
if certPub != nil {
|
||||
return errors.New("go-jose/go-jose: invalid JWK, found 'oct' (symmetric) key with cert chain")
|
||||
}
|
||||
key, err = raw.symmetricKey()
|
||||
@@ -289,8 +272,10 @@ func (k *JSONWebKey) unmarshalJSON(data []byte, allowPrivate bool) (err error) {
|
||||
return ErrUnsupportedKeyType
|
||||
}
|
||||
|
||||
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")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
*k = JSONWebKey{Key: key, KeyID: raw.Kid, Algorithm: raw.Alg, Use: raw.Use, Certificates: certs}
|
||||
@@ -407,9 +392,6 @@ 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
|
||||
@@ -523,41 +505,14 @@ 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) {
|
||||
return rsaPublicKey(key.N, key.E)
|
||||
}
|
||||
|
||||
func rsaPublicKey(N, E *byteBuffer) (*rsa.PublicKey, error) {
|
||||
if N == nil || E == nil {
|
||||
if key.N == nil || key.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: n,
|
||||
E: int(eInt64),
|
||||
N: key.N.bigInt(),
|
||||
E: key.E.toInt(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -601,11 +556,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.bytes()) {
|
||||
if curveSize(curve) != len(key.X.data) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC public key, wrong length for x")
|
||||
}
|
||||
|
||||
if curveSize(curve) != len(key.Y.bytes()) {
|
||||
if curveSize(curve) != len(key.Y.data) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC public key, wrong length for y")
|
||||
}
|
||||
|
||||
@@ -718,21 +673,16 @@ 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: *pubKey,
|
||||
D: key.D.bigInt(),
|
||||
Primes: []*big.Int{p, q},
|
||||
PublicKey: rsa.PublicKey{
|
||||
N: key.N.bigInt(),
|
||||
E: key.E.toInt(),
|
||||
},
|
||||
D: key.D.bigInt(),
|
||||
Primes: []*big.Int{
|
||||
key.P.bigInt(),
|
||||
key.Q.bigInt(),
|
||||
},
|
||||
}
|
||||
|
||||
if key.Dp != nil {
|
||||
@@ -745,7 +695,7 @@ func (key rawJSONWebKey) rsaPrivateKey() (*rsa.PrivateKey, error) {
|
||||
rv.Precomputed.Qinv = key.Qi.bigInt()
|
||||
}
|
||||
|
||||
err = rv.Validate()
|
||||
err := rv.Validate()
|
||||
return rv, err
|
||||
}
|
||||
|
||||
@@ -819,16 +769,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.bytes()) {
|
||||
if curveSize(curve) != len(key.X.data) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for x")
|
||||
}
|
||||
|
||||
if curveSize(curve) != len(key.Y.bytes()) {
|
||||
if curveSize(curve) != len(key.Y.data) {
|
||||
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.bytes()) {
|
||||
if dSize(curve) != len(key.D.data) {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, wrong length for d")
|
||||
}
|
||||
|
||||
|
||||
+33
-80
@@ -27,12 +27,6 @@ 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"`
|
||||
@@ -92,11 +86,6 @@ 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,
|
||||
@@ -119,9 +108,6 @@ 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,
|
||||
@@ -139,9 +125,6 @@ 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,
|
||||
@@ -174,11 +157,7 @@ func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature
|
||||
authData.WriteString(signature.original.Protected.base64())
|
||||
} else if signature.protected != nil {
|
||||
protectedHeader = signature.protected
|
||||
serializedProtected, err := json.Marshal(protectedHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authData.WriteString(base64.RawURLEncoding.EncodeToString(serializedProtected))
|
||||
authData.WriteString(base64.RawURLEncoding.EncodeToString(mustSerializeJSON(protectedHeader)))
|
||||
}
|
||||
|
||||
needsBase64 := true
|
||||
@@ -201,13 +180,9 @@ func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature
|
||||
return authData.Bytes(), nil
|
||||
}
|
||||
|
||||
// ParseSignedJSON parses a message in JWS JSON Serialization. Messages with
|
||||
// more than [MaxSignatures] signatures are rejected.
|
||||
// ParseSignedJSON parses a message in JWS JSON Serialization.
|
||||
//
|
||||
// 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,
|
||||
@@ -261,9 +236,6 @@ 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(),
|
||||
@@ -273,13 +245,14 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
if len(parsed.Signatures) == 0 {
|
||||
// No signatures array, must be flattened serialization
|
||||
signature := Signature{}
|
||||
prot, err := parseProtectedHeader(parsed.Protected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
signature.protected = prot
|
||||
|
||||
// Check that there is not a nonce in the unprotected header
|
||||
if parsed.Header != nil && parsed.Header.getNonce() != "" {
|
||||
return nil, ErrUnprotectedNonce
|
||||
@@ -302,17 +275,17 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
Signature: parsed.Signature,
|
||||
}
|
||||
|
||||
mergedHeaders := signature.mergedHeaders()
|
||||
alg := mergedHeaders.getSignatureAlgorithm()
|
||||
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
|
||||
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
|
||||
}
|
||||
|
||||
signature.Header, err = mergedHeaders.sanitized()
|
||||
var err error
|
||||
signature.Header, err = signature.mergedHeaders().sanitized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alg := SignatureAlgorithm(signature.Header.Algorithm)
|
||||
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
|
||||
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
|
||||
}
|
||||
|
||||
if signature.header != nil {
|
||||
signature.Unprotected, err = signature.header.sanitized()
|
||||
if err != nil {
|
||||
@@ -337,31 +310,30 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
}
|
||||
|
||||
for i, sig := range parsed.Signatures {
|
||||
prot, err := parseProtectedHeader(sig.Protected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
obj.Signatures[i].header = sig.Header
|
||||
|
||||
mergedHeaders := obj.Signatures[i].mergedHeaders()
|
||||
alg := mergedHeaders.getSignatureAlgorithm()
|
||||
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
|
||||
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
|
||||
}
|
||||
|
||||
obj.Signatures[i].Header, err = mergedHeaders.sanitized()
|
||||
var err error
|
||||
obj.Signatures[i].Header, err = obj.Signatures[i].mergedHeaders().sanitized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alg := SignatureAlgorithm(obj.Signatures[i].Header.Algorithm)
|
||||
if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
|
||||
return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
|
||||
}
|
||||
|
||||
if obj.Signatures[i].header != nil {
|
||||
obj.Signatures[i].Unprotected, err = obj.Signatures[i].header.sanitized()
|
||||
if err != nil {
|
||||
@@ -387,6 +359,7 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo
|
||||
// Copy value of sig
|
||||
original := sig
|
||||
|
||||
obj.Signatures[i].header = sig.Header
|
||||
obj.Signatures[i].original = &original
|
||||
}
|
||||
|
||||
@@ -451,10 +424,7 @@ func (obj JSONWebSignature) compactSerialize(detached bool) (string, error) {
|
||||
return "", ErrNotSupported
|
||||
}
|
||||
|
||||
serializedProtected, err := json.Marshal(obj.Signatures[0].protected)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
serializedProtected := mustSerializeJSON(obj.Signatures[0].protected)
|
||||
|
||||
var payload []byte
|
||||
if !detached {
|
||||
@@ -479,23 +449,14 @@ 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, err := json.Marshal(obj.Signatures[0].protected)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
serializedProtected := mustSerializeJSON(obj.Signatures[0].protected)
|
||||
raw.Protected = newBuffer(serializedProtected)
|
||||
}
|
||||
raw.Header = obj.Signatures[0].header
|
||||
@@ -509,18 +470,10 @@ func (obj JSONWebSignature) FullSerialize() string {
|
||||
}
|
||||
|
||||
if signature.protected != nil {
|
||||
serializedProtected, err := json.Marshal(signature.protected)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
raw.Signatures[i].Protected = newBuffer(serializedProtected)
|
||||
raw.Signatures[i].Protected = newBuffer(mustSerializeJSON(signature.protected))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
return string(mustSerializeJSON(raw))
|
||||
}
|
||||
|
||||
+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 failed, token issued in the future (iat)")
|
||||
var ErrIssuedInTheFuture = errors.New("go-jose/go-jose/jwt: validation field, 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)")
|
||||
|
||||
+17
-12
@@ -85,12 +85,7 @@ func (t *NestedJSONWebToken) Decrypt(decryptionKey interface{}) (*JSONWebToken,
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
// ParseSigned parses token from JWS form.
|
||||
func ParseSigned(s string, signatureAlgorithms []jose.SignatureAlgorithm) (*JSONWebToken, error) {
|
||||
sig, err := jose.ParseSignedCompact(s, signatureAlgorithms)
|
||||
if err != nil {
|
||||
@@ -147,10 +142,13 @@ func parseEncryptedCompact(
|
||||
return enc, nil
|
||||
}
|
||||
|
||||
// ParseEncrypted parses a JWT in the JWE Compact Serialization.
|
||||
// ParseEncrypted parses token from JWE form.
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
func ParseEncrypted(s string,
|
||||
keyAlgorithms []jose.KeyAlgorithm,
|
||||
contentEncryption []jose.ContentEncryption,
|
||||
@@ -166,10 +164,17 @@ func ParseEncrypted(s string,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseSignedAndEncrypted parses a signed-then-encrypted token in the JWE Compact Serialization.
|
||||
// ParseSignedAndEncrypted parses signed-then-encrypted token from JWE form.
|
||||
//
|
||||
// The encryptionKeyAlgorithms, contentEncryption, and signatureAlgorithms parameters contain the allowable
|
||||
// algorithms. They must be non-empty. Non-matching algorithms cause a parse error.
|
||||
// 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.
|
||||
func ParseSignedAndEncrypted(s string,
|
||||
encryptionKeyAlgorithms []jose.KeyAlgorithm,
|
||||
contentEncryption []jose.ContentEncryption,
|
||||
|
||||
+37
-43
@@ -18,7 +18,6 @@ package jose
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
@@ -211,20 +210,13 @@ 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, any certificate in the
|
||||
// chain has an RSA key larger than [MaxRSAModulusBits], or the chain could
|
||||
// an error if there was no x5c header present 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()
|
||||
@@ -305,10 +297,9 @@ func (parsed rawHeader) getEPK() (*JSONWebKey, error) {
|
||||
if v == nil {
|
||||
return nil, 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 {
|
||||
var epk *JSONWebKey
|
||||
err := json.Unmarshal(*v, &epk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return epk, nil
|
||||
@@ -334,6 +325,20 @@ 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) {
|
||||
@@ -419,14 +424,10 @@ func (parsed rawHeader) sanitized() (h Header, err error) {
|
||||
}
|
||||
switch k {
|
||||
case headerJWK:
|
||||
// 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)
|
||||
var jwk *JSONWebKey
|
||||
err = json.Unmarshal(*v, &jwk)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal JWK: %w", err)
|
||||
err = fmt.Errorf("failed to unmarshal JWK: %v: %#v", err, string(*v))
|
||||
return
|
||||
}
|
||||
h.JSONWebKey = jwk
|
||||
@@ -498,8 +499,22 @@ func parseCertificateChain(chain []string) ([]*x509.Certificate, error) {
|
||||
}
|
||||
|
||||
func (parsed rawHeader) isSet(k HeaderKey) bool {
|
||||
_, ok := parsed[k]
|
||||
return ok
|
||||
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
|
||||
}
|
||||
|
||||
// Merge headers from src into dst, giving precedence to headers from l.
|
||||
@@ -549,24 +564,3 @@ 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
|
||||
}
|
||||
|
||||
+1
-4
@@ -321,10 +321,7 @@ func (ctx *genericSigner) Sign(payload []byte) (*JSONWebSignature, error) {
|
||||
protected[k] = v
|
||||
}
|
||||
|
||||
serializedProtected, err := json.Marshal(protected)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: Error marshaling JSON: %s", err)
|
||||
}
|
||||
serializedProtected := mustSerializeJSON(protected)
|
||||
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.bytes()) == 0 {
|
||||
if p2s == nil || len(p2s.data) == 0 {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: invalid P2S: must be present")
|
||||
}
|
||||
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
# 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
@@ -1,25 +0,0 @@
|
||||
# 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.
|
||||
|
||||
-2
@@ -6,9 +6,7 @@ This package provides various compression algorithms.
|
||||
* [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) is a high performance replacement for Snappy.
|
||||
* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
|
||||
* [snappy](https://github.com/klauspost/compress/tree/master/snappy) is a drop-in replacement for `github.com/golang/snappy` offering better compression and concurrent streams.
|
||||
* [lzw](https://github.com/klauspost/compress/tree/master/lzw) is a drop-in replacement for `compress/lzw` with 1.4-4x faster decompression and 1.1-2.7x faster compression, depending on the data.
|
||||
* [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding.
|
||||
* [Xpress](https://github.com/klauspost/compress/tree/master/xpress) decompression of the Microsoft XPRESS (MS-XCA) plain LZ77 and LZ77+Huffman formats (the LZ77+Huffman variant is the one used in WIM images and Windows Compact OS / WOF data).
|
||||
* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped/zstd HTTP requests efficiently.
|
||||
* [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation.
|
||||
|
||||
|
||||
+1
-2
@@ -659,8 +659,7 @@ func (s *Scratch) validateNorm() (err error) {
|
||||
if v >= 0 {
|
||||
total += int(v)
|
||||
} else {
|
||||
// -1 takes one cell. Adding -v would let a wrapped -32768 pass.
|
||||
total++
|
||||
total -= int(v)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
|
||||
-6
@@ -3,7 +3,6 @@ package fse
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -124,11 +123,6 @@ func (s *Scratch) readNCount() error {
|
||||
remaining -= count
|
||||
gotTotal += count
|
||||
}
|
||||
if count > math.MaxInt16 {
|
||||
// Only one symbol filling a tableLog 15 table, which cannot be decoded.
|
||||
// Truncated, it would leave the decoding table unwritten.
|
||||
return fmt.Errorf("corruption detected (count %d > %d)", count, math.MaxInt16)
|
||||
}
|
||||
s.norm[charnum&0xff] = int16(count)
|
||||
charnum++
|
||||
previous0 = count == 0
|
||||
|
||||
-63
@@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/bits"
|
||||
|
||||
"github.com/klauspost/compress/internal/le"
|
||||
)
|
||||
@@ -211,68 +210,6 @@ func (b *bitReaderShifted) remaining() uint {
|
||||
return b.off*8 + uint(64-b.bitsRead)
|
||||
}
|
||||
|
||||
// canUseAsm reports whether the reader has a full 8-byte window ahead of
|
||||
// its read pointer, which the Decompress4X asm loops need to take over.
|
||||
func (b *bitReaderShifted) canUseAsm() bool {
|
||||
return b.off >= 8
|
||||
}
|
||||
|
||||
// prepareForAsm establishes the invariant the Decompress4X asm loops rely
|
||||
// on: value == load64(in[off:off+8]) << bitsRead with bitsRead <= 7, so
|
||||
// that a full group of symbols can never shift their sentinel bit out of
|
||||
// the container. init leaves bitsRead == 8 when the final byte of the
|
||||
// stream is exactly 0x01; that whole byte is consumed, so the same position
|
||||
// is the window one byte lower with nothing consumed. Requires canUseAsm.
|
||||
func (b *bitReaderShifted) prepareForAsm() {
|
||||
if b.bitsRead >= 8 {
|
||||
b.off--
|
||||
b.value = le.Load64(b.in, b.off)
|
||||
b.bitsRead -= 8
|
||||
}
|
||||
}
|
||||
|
||||
// restoreFromAsm converts the state left behind by the Decompress4X asm
|
||||
// loops back to the invariant the Go code relies on: value holds the 8
|
||||
// bytes at in[off:off+8] shifted left by bitsRead, with the low bitsRead
|
||||
// bits zero.
|
||||
//
|
||||
// The asm keeps a sentinel bit in value just below the unread bits, so its
|
||||
// trailing zero count is the number of consumed bits. The sentinel is ORed
|
||||
// over the lowest bit of the window, which the loop never consumes before
|
||||
// re-reading memory, so the window is re-read here too rather than taken
|
||||
// from value. The asm reports off as the signed distance from the start of
|
||||
// the stream, and it can be negative: a reload always reads a whole 8-byte
|
||||
// window, so a stream that is nearly drained ends with up to 7 bytes of the
|
||||
// previous stream (or the jump table) below its start inside the window.
|
||||
// Those bytes sit below the stream's own bits and count as consumed.
|
||||
// Anything further below, or more bits consumed than the stream holds, is
|
||||
// corruption.
|
||||
func (b *bitReaderShifted) restoreFromAsm() error {
|
||||
off := int(b.off)
|
||||
consumed := uint(bits.TrailingZeros64(b.value))
|
||||
if off < 0 {
|
||||
if off < -7 {
|
||||
return errors.New("corruption detected: stream underrun")
|
||||
}
|
||||
consumed += uint(-off) * 8
|
||||
if consumed > 64 {
|
||||
return errors.New("corruption detected: stream underrun")
|
||||
}
|
||||
off = 0
|
||||
}
|
||||
if off+8 > len(b.in) {
|
||||
return errors.New("corruption detected: stream overrun")
|
||||
}
|
||||
b.off = uint(off)
|
||||
b.bitsRead = uint8(consumed)
|
||||
if consumed >= 64 {
|
||||
b.value = 0
|
||||
} else {
|
||||
b.value = le.Load64(b.in, b.off) << consumed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// close the bitstream and returns an error if out-of-buffer reads occurred.
|
||||
func (b *bitReaderShifted) close() error {
|
||||
// Release reference.
|
||||
|
||||
-4
@@ -54,10 +54,6 @@ func ReadTable(in []byte, s *Scratch) (s2 *Scratch, remain []byte, err error) {
|
||||
if len(in) < int(iSize) {
|
||||
return s, nil, fmt.Errorf("input too small for table, want %d bytes, have %d", iSize, len(in))
|
||||
}
|
||||
// The low nibble is the weights' FSE tableLog - 5, which the format limits to 6.
|
||||
if in[0]&15+5 > 6 {
|
||||
return s, nil, errors.New("corrupt input: weights tableLog too big")
|
||||
}
|
||||
// FSE compressed weights
|
||||
s.fse.DecompressLimit = 255
|
||||
hw := s.huffWeight[:]
|
||||
|
||||
+8
-87
@@ -8,99 +8,36 @@ import (
|
||||
)
|
||||
|
||||
// decompress4x_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress4X when tablelog > 8, decoding fastSymbols symbols per
|
||||
// stream between bit container reloads.
|
||||
// of Decompress4X when tablelog > 8.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_main_loop_amd64(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_8b_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress4X when tablelog <= 8, decoding fast8bSymbols symbols
|
||||
// per stream between bit container reloads.
|
||||
// of Decompress4X when tablelog <= 8 which decodes 4 entries
|
||||
// per loop.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_8b_main_loop_amd64(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_4b_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress4X when tablelog <= 4, decoding fast4bSymbols symbols
|
||||
// per stream between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_4b_main_loop_amd64(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_4b_main_loop_bmi2 is the BMI2 twin of decompress4x_4b_main_loop_amd64.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_4b_main_loop_bmi2(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_main_loop_bmi2 is the BMI2 twin of decompress4x_main_loop_amd64.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_main_loop_bmi2(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_8b_main_loop_bmi2 is the BMI2 twin of decompress4x_8b_main_loop_amd64.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_8b_main_loop_bmi2(ctx *decompress4xContext)
|
||||
|
||||
// decompress1x_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress1X when tablelog > 8, decoding fastSymbols symbols
|
||||
// between bit container reloads.
|
||||
// of Decompress1X when tablelog > 8.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_main_loop_amd64(ctx *decompress1xContext)
|
||||
|
||||
// decompress1x_8b_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress1X when tablelog <= 8, decoding fast8bSymbols symbols
|
||||
// between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_8b_main_loop_amd64(ctx *decompress1xContext)
|
||||
|
||||
// decompress1x_4b_main_loop_amd64 is an x86 assembler implementation
|
||||
// of Decompress1X when tablelog <= 4, decoding fast4bSymbols symbols
|
||||
// between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_4b_main_loop_amd64(ctx *decompress1xContext)
|
||||
|
||||
// decompress1x_main_loop_bmi2 is the BMI2 twin of decompress1x_main_loop_amd64.
|
||||
// decompress1x_main_loop_bmi2 is an x86 with BMI2 assembler implementation
|
||||
// of Decompress1X when tablelog > 8.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_main_loop_bmi2(ctx *decompress1xContext)
|
||||
|
||||
// decompress1x_8b_main_loop_bmi2 is the BMI2 twin of decompress1x_8b_main_loop_amd64.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_8b_main_loop_bmi2(ctx *decompress1xContext)
|
||||
|
||||
// decompress1x_4b_main_loop_bmi2 is the BMI2 twin of decompress1x_4b_main_loop_amd64.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_4b_main_loop_bmi2(ctx *decompress1xContext)
|
||||
|
||||
func decompress4x_main_loop_asm(ctx *decompress4xContext) {
|
||||
if cpuinfo.HasBMI2() {
|
||||
decompress4x_main_loop_bmi2(ctx)
|
||||
} else {
|
||||
decompress4x_main_loop_amd64(ctx)
|
||||
}
|
||||
decompress4x_main_loop_amd64(ctx)
|
||||
}
|
||||
|
||||
func decompress4x_8b_main_loop_asm(ctx *decompress4xContext) {
|
||||
if cpuinfo.HasBMI2() {
|
||||
decompress4x_8b_main_loop_bmi2(ctx)
|
||||
} else {
|
||||
decompress4x_8b_main_loop_amd64(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func decompress4x_4b_main_loop_asm(ctx *decompress4xContext) {
|
||||
if cpuinfo.HasBMI2() {
|
||||
decompress4x_4b_main_loop_bmi2(ctx)
|
||||
} else {
|
||||
decompress4x_4b_main_loop_amd64(ctx)
|
||||
}
|
||||
decompress4x_8b_main_loop_amd64(ctx)
|
||||
}
|
||||
|
||||
func decompress1x_main_loop_asm(ctx *decompress1xContext) {
|
||||
@@ -110,19 +47,3 @@ func decompress1x_main_loop_asm(ctx *decompress1xContext) {
|
||||
decompress1x_main_loop_amd64(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func decompress1x_8b_main_loop_asm(ctx *decompress1xContext) {
|
||||
if cpuinfo.HasBMI2() {
|
||||
decompress1x_8b_main_loop_bmi2(ctx)
|
||||
} else {
|
||||
decompress1x_8b_main_loop_amd64(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func decompress1x_4b_main_loop_asm(ctx *decompress1xContext) {
|
||||
if cpuinfo.HasBMI2() {
|
||||
decompress1x_4b_main_loop_bmi2(ctx)
|
||||
} else {
|
||||
decompress1x_4b_main_loop_amd64(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
+755
-3311
File diff suppressed because it is too large
Load Diff
+4
-39
@@ -6,47 +6,24 @@
|
||||
package huff0
|
||||
|
||||
// decompress4x_main_loop_arm64 is an arm64 assembler implementation
|
||||
// of Decompress4X when tablelog > 8, decoding fastSymbols symbols per
|
||||
// stream between bit container reloads.
|
||||
// of Decompress4X when tablelog > 8.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_main_loop_arm64(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_8b_main_loop_arm64 is an arm64 assembler implementation
|
||||
// of Decompress4X when tablelog <= 8, decoding fast8bSymbols symbols
|
||||
// per stream between bit container reloads.
|
||||
// of Decompress4X when tablelog <= 8 which decodes 4 entries
|
||||
// per loop.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_8b_main_loop_arm64(ctx *decompress4xContext)
|
||||
|
||||
// decompress4x_4b_main_loop_arm64 is an arm64 assembler implementation
|
||||
// of Decompress4X when tablelog <= 4, decoding fast4bSymbols symbols
|
||||
// per stream between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress4x_4b_main_loop_arm64(ctx *decompress4xContext)
|
||||
|
||||
// decompress1x_main_loop_arm64 is an arm64 assembler implementation
|
||||
// of Decompress1X when tablelog > 8, decoding fastSymbols symbols
|
||||
// between bit container reloads.
|
||||
// of Decompress1X when tablelog > 8.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_main_loop_arm64(ctx *decompress1xContext)
|
||||
|
||||
// decompress1x_8b_main_loop_arm64 is an arm64 assembler implementation
|
||||
// of Decompress1X when tablelog <= 8, decoding fast8bSymbols symbols
|
||||
// between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_8b_main_loop_arm64(ctx *decompress1xContext)
|
||||
|
||||
// decompress1x_4b_main_loop_arm64 is an arm64 assembler implementation
|
||||
// of Decompress1X when tablelog <= 4, decoding fast4bSymbols symbols
|
||||
// between bit container reloads.
|
||||
//
|
||||
//go:noescape
|
||||
func decompress1x_4b_main_loop_arm64(ctx *decompress1xContext)
|
||||
|
||||
func decompress4x_main_loop_asm(ctx *decompress4xContext) {
|
||||
decompress4x_main_loop_arm64(ctx)
|
||||
}
|
||||
@@ -55,18 +32,6 @@ func decompress4x_8b_main_loop_asm(ctx *decompress4xContext) {
|
||||
decompress4x_8b_main_loop_arm64(ctx)
|
||||
}
|
||||
|
||||
func decompress4x_4b_main_loop_asm(ctx *decompress4xContext) {
|
||||
decompress4x_4b_main_loop_arm64(ctx)
|
||||
}
|
||||
|
||||
func decompress1x_main_loop_asm(ctx *decompress1xContext) {
|
||||
decompress1x_main_loop_arm64(ctx)
|
||||
}
|
||||
|
||||
func decompress1x_8b_main_loop_asm(ctx *decompress1xContext) {
|
||||
decompress1x_8b_main_loop_arm64(ctx)
|
||||
}
|
||||
|
||||
func decompress1x_4b_main_loop_asm(ctx *decompress1xContext) {
|
||||
decompress1x_4b_main_loop_arm64(ctx)
|
||||
}
|
||||
|
||||
+804
-1610
File diff suppressed because it is too large
Load Diff
+21
-81
@@ -14,10 +14,6 @@ import (
|
||||
// fallback8BitSize is the size where using Go version is faster.
|
||||
const fallback8BitSize = 800
|
||||
|
||||
// decompress4xContext is the argument block of the Decompress4X asm loops.
|
||||
// Go fills every field but decoded and inner; the asm advances ip, and on
|
||||
// return leaves each bit reader in the form bitReaderShifted.restoreFromAsm
|
||||
// expects.
|
||||
type decompress4xContext struct {
|
||||
pbr *[4]bitReaderShifted
|
||||
peekBits uint8
|
||||
@@ -25,20 +21,9 @@ type decompress4xContext struct {
|
||||
dstEvery int
|
||||
tbl *dEntrySingle
|
||||
decoded int
|
||||
limit *byte // stream 0's output pointer must stay below this
|
||||
ilowest *byte // start of the input block; no read goes below it
|
||||
ip [4]*byte // each stream's 8-byte input window, advanced by the asm
|
||||
inner *byte // scratch for the asm: the current inner-loop limit
|
||||
limit *byte
|
||||
}
|
||||
|
||||
// Symbols decoded per stream between reloads by the 4X asm loops; must
|
||||
// match the constants of the same names in _generate/gen.go.
|
||||
const (
|
||||
fastSymbols = 5 // tablelog 9..11
|
||||
fast8bSymbols = 7 // tablelog 5..8
|
||||
fast4bSymbols = 14 // tablelog <= 4
|
||||
)
|
||||
|
||||
// Decompress4X will decompress a 4X encoded stream.
|
||||
// The length of the supplied input must match the end of a block exactly.
|
||||
// The *capacity* of the dst slice must match the destination size of
|
||||
@@ -87,55 +72,23 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
|
||||
|
||||
var decoded int
|
||||
|
||||
nSyms := fastSymbols
|
||||
if d.actualTableLog <= 4 {
|
||||
nSyms = fast4bSymbols
|
||||
} else if use8BitTables {
|
||||
nSyms = fast8bSymbols
|
||||
}
|
||||
// The asm writes nSyms bytes per stream per iteration and only re-checks
|
||||
// its bounds between batches of iterations (the batch size is derived
|
||||
// from limit in _generate/gen.go), so stream 0 must stop early enough
|
||||
// that stream 3 (which may be up to 3 bytes shorter than dstEvery)
|
||||
// never writes past the end of out. Every stream needs a full 8-byte
|
||||
// window ahead of its read pointer to enter the loop.
|
||||
if limit := dstEvery - nSyms - 2; limit > 0 && br[0].canUseAsm() && br[1].canUseAsm() && br[2].canUseAsm() && br[3].canUseAsm() {
|
||||
for i := range br {
|
||||
br[i].prepareForAsm()
|
||||
}
|
||||
if len(out) > 4*4 && !(br[0].off < 4 || br[1].off < 4 || br[2].off < 4 || br[3].off < 4) {
|
||||
ctx := decompress4xContext{
|
||||
pbr: &br,
|
||||
peekBits: uint8((64 - d.actualTableLog) & 63), // see: bitReaderShifted.peekBitsFast()
|
||||
out: &out[0],
|
||||
dstEvery: dstEvery,
|
||||
tbl: &single[0],
|
||||
limit: &out[limit],
|
||||
// The 6-byte jump table sits below stream 0 inside src, so the
|
||||
// lowest window may reach into it; restoreFromAsm accounts for
|
||||
// bytes below a stream's start. Bounding by src rather than by
|
||||
// stream 0 keeps the asm running about six bytes longer, which
|
||||
// matters for streams with very short codes.
|
||||
ilowest: &src[0],
|
||||
limit: &out[dstEvery-4], // Always stop decoding when first buffer gets here to avoid writing OOB on last.
|
||||
}
|
||||
for i := range br {
|
||||
ctx.ip[i] = &br[i].in[br[i].off]
|
||||
}
|
||||
switch nSyms {
|
||||
case fast4bSymbols:
|
||||
decompress4x_4b_main_loop_asm(&ctx)
|
||||
case fast8bSymbols:
|
||||
if use8BitTables {
|
||||
decompress4x_8b_main_loop_asm(&ctx)
|
||||
default:
|
||||
} else {
|
||||
decompress4x_main_loop_asm(&ctx)
|
||||
}
|
||||
|
||||
decoded = ctx.decoded
|
||||
out = out[decoded/4:]
|
||||
for i := range br {
|
||||
if err := br[i].restoreFromAsm(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decode remaining.
|
||||
@@ -175,20 +128,18 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// decompress1xContext is the argument block of the Decompress1X asm loops.
|
||||
// Go fills every field but decoded; the asm advances ip, and on return
|
||||
// leaves the bit reader in the form bitReaderShifted.restoreFromAsm expects.
|
||||
type decompress1xContext struct {
|
||||
pbr *bitReaderShifted
|
||||
peekBits uint8
|
||||
out *byte
|
||||
outCap int // no write reaches out[outCap]
|
||||
outCap int
|
||||
tbl *dEntrySingle
|
||||
decoded int
|
||||
ilowest *byte // start of the stream; no read goes below it
|
||||
ip *byte // the 8-byte input window, advanced by the asm
|
||||
}
|
||||
|
||||
// Error reported by asm implementations
|
||||
const error_max_decoded_size_exeeded = -1
|
||||
|
||||
// Decompress1X will decompress a 1X encoded stream.
|
||||
// The cap of the output buffer will be the maximum decompressed size.
|
||||
// The length of the supplied input must match the end of a block exactly.
|
||||
@@ -207,36 +158,25 @@ func (d *Decoder) Decompress1X(dst, src []byte) ([]byte, error) {
|
||||
const tlSize = 1 << tableLogMax
|
||||
const tlMask = tlSize - 1
|
||||
|
||||
// The asm decodes whole batches of symbols and only re-checks its
|
||||
// bounds between them, so it needs at least one batch of room (the
|
||||
// output bound rounds nSyms up to 16, see _generate/gen.go) and a full
|
||||
// 8-byte window ahead of the read pointer to enter the loop. It also
|
||||
// needs at most 7 bits consumed on entry so that a batch cannot run
|
||||
// the container dry, which prepareForAsm establishes.
|
||||
decoded := 0
|
||||
if maxDecodedSize >= 16 && br.canUseAsm() {
|
||||
br.prepareForAsm()
|
||||
if maxDecodedSize >= 4 {
|
||||
ctx := decompress1xContext{
|
||||
pbr: &br,
|
||||
out: &dst[0],
|
||||
outCap: maxDecodedSize,
|
||||
peekBits: uint8((64 - d.actualTableLog) & 63), // see: bitReaderShifted.peekBitsFast()
|
||||
tbl: &d.dt.single[0],
|
||||
ilowest: &src[0],
|
||||
ip: &br.in[br.off],
|
||||
}
|
||||
if d.actualTableLog <= 4 {
|
||||
decompress1x_4b_main_loop_asm(&ctx)
|
||||
} else if d.actualTableLog <= 8 {
|
||||
decompress1x_8b_main_loop_asm(&ctx)
|
||||
} else {
|
||||
decompress1x_main_loop_asm(&ctx)
|
||||
}
|
||||
decoded = ctx.decoded
|
||||
}
|
||||
dst = dst[:decoded]
|
||||
|
||||
bitsLeft := br.remaining()
|
||||
decompress1x_main_loop_asm(&ctx)
|
||||
if ctx.decoded == error_max_decoded_size_exeeded {
|
||||
return nil, ErrMaxDecodedSizeExceeded
|
||||
}
|
||||
|
||||
dst = dst[:ctx.decoded]
|
||||
}
|
||||
|
||||
// br < 8, so uint8 is fine
|
||||
bitsLeft := uint8(br.off)*8 + 64 - br.bitsRead
|
||||
for bitsLeft > 0 {
|
||||
br.fill()
|
||||
if len(dst) >= maxDecodedSize {
|
||||
@@ -246,7 +186,7 @@ func (d *Decoder) Decompress1X(dst, src []byte) ([]byte, error) {
|
||||
v := d.dt.single[br.peekBitsFast(d.actualTableLog)&tlMask]
|
||||
nBits := uint8(v.entry)
|
||||
br.advance(nBits)
|
||||
bitsLeft -= uint(nBits)
|
||||
bitsLeft -= nBits
|
||||
dst = append(dst, uint8(v.entry>>8))
|
||||
}
|
||||
return dst, br.close()
|
||||
|
||||
-3
@@ -240,11 +240,9 @@ func (b *blockDec) decodeBuf(hist *history) error {
|
||||
b.dst[i] = v
|
||||
}
|
||||
hist.appendKeep(b.dst)
|
||||
hist.decoders.consumeSyncLen(len(b.dst))
|
||||
return nil
|
||||
case blockTypeRaw:
|
||||
hist.appendKeep(b.data)
|
||||
hist.decoders.consumeSyncLen(len(b.data))
|
||||
return nil
|
||||
case blockTypeCompressed:
|
||||
saved := b.dst
|
||||
@@ -489,7 +487,6 @@ func (b *blockDec) decodeCompressed(hist *history) error {
|
||||
}
|
||||
if hist.decoders.nSeqs == 0 {
|
||||
b.dst = append(b.dst, hist.decoders.literals...)
|
||||
hist.decoders.consumeSyncLen(len(hist.decoders.literals))
|
||||
return nil
|
||||
}
|
||||
before := len(hist.decoders.out)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user