Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
implement docker trust as plugin
move the `trust` subcommands to a plugin, so that the subcommands can
be installed separate from the `docker trust` integration in push/pull
(for situations where trust verification happens on the daemon side).

    make binary
    go build -o /usr/libexec/docker/cli-plugins/docker-trust ./cmd/docker-trust

    docker info
    Client:
     Version:    28.2.0-dev
     Context:    default
     Debug Mode: false
     Plugins:
      buildx: Docker Buildx (Docker Inc.)
        Version:  v0.24.0
        Path:     /usr/libexec/docker/cli-plugins/docker-buildx
      trust: Manage trust on Docker images (Docker Inc.)
        Version:  unknown-version
        Path:     /usr/libexec/docker/cli-plugins/docker-trust

    docker trust --help
    Usage:  docker trust [OPTIONS] COMMAND

    Extended build capabilities with BuildKit

    Options:
      -D, --debug   Enable debug logging

    Management Commands:
      key         Manage keys for signing Docker images
      signer      Manage entities who can sign Docker images

    Commands:
      inspect     Return low-level information about keys and signatures
      revoke      Remove trust for an image
      sign        Sign an image

    Run 'docker trust COMMAND --help' for more information on a command.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
  • Loading branch information
thaJeztah committed Nov 6, 2025
commit c9bb29115455f56fde4600ff676b7e0a795d5a2e
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ dynbinary: ## build dynamically linked binary
plugins: ## build example CLI plugins
scripts/build/plugins

.PHONY: trust-plugin
trust-plugin: ## build docker-trust CLI plugins
scripts/build/trust-plugin

.PHONY: install-trust-plugin
install-trust-plugin: trust-plugin
install-trust-plugin: ## install docker-trust CLI plugins
install -D -m 0755 "$$(readlink -f build/docker-trust)" /usr/libexec/docker/cli-plugins/docker-trust

.PHONY: vendor
vendor: ## update vendor with go modules
rm -rf vendor
Expand Down
1 change: 0 additions & 1 deletion cli/command/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
_ "github.com/docker/cli/cli/command/stack"
_ "github.com/docker/cli/cli/command/swarm"
_ "github.com/docker/cli/cli/command/system"
_ "github.com/docker/cli/cli/command/trust"
_ "github.com/docker/cli/cli/command/volume"
"github.com/docker/cli/internal/commands"
"github.com/spf13/cobra"
Expand Down
128 changes: 128 additions & 0 deletions cmd/docker-trust/internal/test/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package test

import (
"bytes"
"errors"
"io"
"strings"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/streams"
"github.com/moby/moby/client"
notaryclient "github.com/theupdateframework/notary/client"
)

// NotaryClientFuncType defines a function that returns a fake notary client
type NotaryClientFuncType func() (notaryclient.Repository, error)

// FakeCli emulates the default DockerCli
type FakeCli struct {
command.DockerCli
client client.APIClient
configfile *configfile.ConfigFile
out *streams.Out
outBuffer *bytes.Buffer
err *streams.Out
errBuffer *bytes.Buffer
in *streams.In
server command.ServerInfo
notaryClientFunc NotaryClientFuncType
currentContext string
}

// NewFakeCli returns a fake for the command.Cli interface
func NewFakeCli(apiClient client.APIClient, opts ...func(*FakeCli)) *FakeCli {
outBuffer := new(bytes.Buffer)
errBuffer := new(bytes.Buffer)
c := &FakeCli{
client: apiClient,
out: streams.NewOut(outBuffer),
outBuffer: outBuffer,
err: streams.NewOut(errBuffer),
errBuffer: errBuffer,
in: streams.NewIn(io.NopCloser(strings.NewReader(""))),
// Use an empty string for filename so that tests don't create configfiles
// Set cli.ConfigFile().Filename to a tempfile to support Save.
configfile: configfile.New(""),
currentContext: command.DefaultContextName,
}
for _, opt := range opts {
opt(c)
}
return c
}

// SetIn sets the input of the cli to the specified ReadCloser
func (c *FakeCli) SetIn(in *streams.In) {
c.in = in
}

// SetErr sets the stderr stream for the cli to the specified io.Writer
func (c *FakeCli) SetErr(err *streams.Out) {
c.err = err
}

// SetOut sets the stdout stream for the cli to the specified io.Writer
func (c *FakeCli) SetOut(out *streams.Out) {
c.out = out
}

// Client returns a docker API client
func (c *FakeCli) Client() client.APIClient {
return c.client
}

// CurrentVersion returns the API version used by FakeCli.
// func (*FakeCli) CurrentVersion() string {
// return client.MaxAPIVersion
// }

// Out returns the output stream (stdout) the cli should write on
func (c *FakeCli) Out() *streams.Out {
return c.out
}

// Err returns the output stream (stderr) the cli should write on
func (c *FakeCli) Err() *streams.Out {
return c.err
}

// In returns the input stream the cli will use
func (c *FakeCli) In() *streams.In {
return c.in
}

// ConfigFile returns the cli configfile object (to get client configuration)
func (c *FakeCli) ConfigFile() *configfile.ConfigFile {
return c.configfile
}

// OutBuffer returns the stdout buffer
func (c *FakeCli) OutBuffer() *bytes.Buffer {
return c.outBuffer
}

// ErrBuffer Buffer returns the stderr buffer
func (c *FakeCli) ErrBuffer() *bytes.Buffer {
return c.errBuffer
}

// ResetOutputBuffers resets the .OutBuffer() and.ErrBuffer() back to empty
func (c *FakeCli) ResetOutputBuffers() {
c.outBuffer.Reset()
c.errBuffer.Reset()
}

// SetNotaryClient sets the internal getter for retrieving a NotaryClient
func (c *FakeCli) SetNotaryClient(notaryClientFunc NotaryClientFuncType) {
c.notaryClientFunc = notaryClientFunc
}

// NotaryClient returns an err for testing unless defined
func (c *FakeCli) NotaryClient() (notaryclient.Repository, error) {
if c.notaryClientFunc != nil {
return c.notaryClientFunc()
}
return nil, errors.New("no notary client available unless defined")
}
15 changes: 15 additions & 0 deletions cmd/docker-trust/internal/test/randomid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package test

import (
"crypto/rand"
"encoding/hex"
)

// RandomID returns a unique, 64-character ID consisting of a-z, 0-9.
func RandomID() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(b)
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
114 changes: 114 additions & 0 deletions cmd/docker-trust/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package main

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"syscall"

cerrdefs "github.com/containerd/errdefs"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/version"
"github.com/docker/cli/cmd/docker-trust/trust"
"go.opentelemetry.io/otel"
)

func runStandalone(cmd *command.DockerCli) error {
defer flushMetrics(cmd)
executable := os.Args[0]
rootCmd := trust.NewRootCmd(filepath.Base(executable), false, cmd)
return rootCmd.Execute()
}

// flushMetrics will manually flush metrics from the configured
// meter provider. This is needed when running in standalone mode
// because the meter provider is initialized by the cli library,
// but the mechanism for forcing it to report is not presently
// exposed and not invoked when run in standalone mode.
// There are plans to fix that in the next release, but this is
// needed temporarily until the API for this is more thorough.
func flushMetrics(cmd *command.DockerCli) {
if mp, ok := cmd.MeterProvider().(command.MeterProvider); ok {
if err := mp.ForceFlush(context.Background()); err != nil {
otel.Handle(err)
}
}
}

func runPlugin(cmd *command.DockerCli) error {
rootCmd := trust.NewRootCmd("trust", true, cmd)
return plugin.RunPlugin(cmd, rootCmd, metadata.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: version.Version,
})
}

func run(cmd *command.DockerCli) error {
if plugin.RunningStandalone() {
return runStandalone(cmd)
}
return runPlugin(cmd)
}

type errCtxSignalTerminated struct {
signal os.Signal
}

func (errCtxSignalTerminated) Error() string {
return ""
}

func main() {
cmd, err := command.NewDockerCli()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

if err = run(cmd); err == nil {
return
}

if errors.As(err, &errCtxSignalTerminated{}) {
os.Exit(getExitCode(err))
}

if !cerrdefs.IsCanceled(err) {
if err.Error() != "" {
_, _ = fmt.Fprintln(cmd.Err(), err)
}
os.Exit(getExitCode(err))
}
}

// getExitCode returns the exit-code to use for the given error.
// If err is a [cli.StatusError] and has a StatusCode set, it uses the
// status-code from it, otherwise it returns "1" for any error.
func getExitCode(err error) int {
if err == nil {
return 0
}

var userTerminatedErr errCtxSignalTerminated
if errors.As(err, &userTerminatedErr) {
s, ok := userTerminatedErr.signal.(syscall.Signal)
if !ok {
return 1
}
return 128 + int(s)
}

var stErr cli.StatusError
if errors.As(err, &stErr) && stErr.StatusCode != 0 { // FIXME(thaJeztah): StatusCode should never be used with a zero status-code. Check if we do this anywhere.
return stErr.StatusCode
}

// No status-code provided; all errors should have a non-zero exit code.
return 1
}
File renamed without changes.
81 changes: 81 additions & 0 deletions cmd/docker-trust/trust/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package trust

import (
"fmt"

"github.com/docker/cli-docs-tool/annotation"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/debug"
cliflags "github.com/docker/cli/cli/flags"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

func NewRootCmd(name string, isPlugin bool, dockerCLI *command.DockerCli) *cobra.Command {
var opt rootOptions
cmd := &cobra.Command{
Use: name,
Short: "Manage trust on Docker images",
Long: `Extended build capabilities with BuildKit`,
Annotations: map[string]string{
annotation.CodeDelimiter: `"`,
},
CompletionOptions: cobra.CompletionOptions{
HiddenDefaultCmd: true,
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if opt.debug {
debug.Enable()
}
// cmd.SetContext(appcontext.Context())
if !isPlugin {
// InstallFlags and SetDefaultOptions are necessary to match
// the plugin mode behavior to handle env vars such as
// DOCKER_TLS, DOCKER_TLS_VERIFY, ... and we also need to use a
// new flagset to avoid conflict with the global debug flag
// that we already handle in the root command otherwise it
// would panic.
nflags := pflag.NewFlagSet(cmd.DisplayName(), pflag.ContinueOnError)
options := cliflags.NewClientOptions()
options.InstallFlags(nflags)
options.SetDefaultOptions(nflags)
return dockerCLI.Initialize(options)
}
return plugin.PersistentPreRunE(cmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
_ = cmd.Help()
return cli.StatusError{
StatusCode: 1,
Status: fmt.Sprintf("ERROR: unknown command: %q", args[0]),
}
},
}
if !isPlugin {
// match plugin behavior for standalone mode
// https://github.com/docker/cli/blob/6c9eb708fa6d17765d71965f90e1c59cea686ee9/cli-plugins/plugin/plugin.go#L117-L127
cmd.SilenceUsage = true
cmd.SilenceErrors = true
cmd.TraverseChildren = true
cmd.DisableFlagsInUseLine = true
}

cmd.AddCommand(
newRevokeCommand(dockerCLI),
newSignCommand(dockerCLI),
newTrustKeyCommand(dockerCLI),
newTrustSignerCommand(dockerCLI),
newInspectCommand(dockerCLI),
)

return cmd
}

type rootOptions struct {
debug bool
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/cmd/docker-trust/internal/trust"
"github.com/fvbommel/sortorder"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/sirupsen/logrus"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package trust
import (
"testing"

"github.com/docker/cli/cli/trust"
"github.com/docker/cli/cmd/docker-trust/internal/trust"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
"gotest.tools/v3/assert"
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"

"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cmd/docker-trust/internal/test"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
Expand Down
Loading