Merge pull request #2471 from tonistiigi/v0.14.1-picks

[v0.14] cherry picks for v0.14.1
This commit is contained in:
Tõnis Tiigi 2024-05-22 07:43:43 -07:00 committed by GitHub
commit 59582a88fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 331 additions and 179 deletions

View File

@ -541,7 +541,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
} }
if pushNames != "" { if pushNames != "" {
progress.Write(pw, fmt.Sprintf("merging manifest list %s", pushNames), func() error { err := progress.Write(pw, fmt.Sprintf("merging manifest list %s", pushNames), func() error {
descs := make([]specs.Descriptor, 0, len(res)) descs := make([]specs.Descriptor, 0, len(res))
for _, r := range res { for _, r := range res {
@ -637,6 +637,9 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
} }
return nil return nil
}) })
if err != nil {
return err
}
} }
return nil return nil
}) })
@ -781,11 +784,11 @@ func calculateChildTargets(reqs map[string][]*reqForNode, opt map[string]Options
} }
func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *client.SolveOpt) error { func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *client.SolveOpt) error {
m := map[string]string{} m := map[string][]string{}
for k, v := range so.FrontendAttrs { for k, v := range so.FrontendAttrs {
if strings.HasPrefix(k, "context:") && strings.HasPrefix(v, "target:") { if strings.HasPrefix(k, "context:") && strings.HasPrefix(v, "target:") {
target := resultKey(index, strings.TrimPrefix(v, "target:")) target := resultKey(index, strings.TrimPrefix(v, "target:"))
m[target] = k m[target] = append(m[target], k)
} }
} }
if len(m) == 0 { if len(m) == 0 {
@ -800,7 +803,7 @@ func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *c
return err return err
} }
for k, v := range m { for k, contexts := range m {
r, ok := res[k] r, ok := res[k]
if !ok { if !ok {
continue continue
@ -815,19 +818,45 @@ func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *c
if so.FrontendInputs == nil { if so.FrontendInputs == nil {
so.FrontendInputs = map[string]llb.State{} so.FrontendInputs = map[string]llb.State{}
} }
if len(rr.Refs) > 0 {
for platform, r := range rr.Refs { for _, v := range contexts {
st, err := r.ToState() if len(rr.Refs) > 0 {
for platform, r := range rr.Refs {
st, err := r.ToState()
if err != nil {
return err
}
so.FrontendInputs[k+"::"+platform] = st
so.FrontendAttrs[v+"::"+platform] = "input:" + k + "::" + platform
metadata := make(map[string][]byte)
if dt, ok := rr.Metadata[exptypes.ExporterImageConfigKey+"/"+platform]; ok {
metadata[exptypes.ExporterImageConfigKey] = dt
}
if dt, ok := rr.Metadata["containerimage.buildinfo/"+platform]; ok {
metadata["containerimage.buildinfo"] = dt
}
if len(metadata) > 0 {
dt, err := json.Marshal(metadata)
if err != nil {
return err
}
so.FrontendAttrs["input-metadata:"+k+"::"+platform] = string(dt)
}
}
delete(so.FrontendAttrs, v)
}
if rr.Ref != nil {
st, err := rr.Ref.ToState()
if err != nil { if err != nil {
return err return err
} }
so.FrontendInputs[k+"::"+platform] = st so.FrontendInputs[k] = st
so.FrontendAttrs[v+"::"+platform] = "input:" + k + "::" + platform so.FrontendAttrs[v] = "input:" + k
metadata := make(map[string][]byte) metadata := make(map[string][]byte)
if dt, ok := rr.Metadata[exptypes.ExporterImageConfigKey+"/"+platform]; ok { if dt, ok := rr.Metadata[exptypes.ExporterImageConfigKey]; ok {
metadata[exptypes.ExporterImageConfigKey] = dt metadata[exptypes.ExporterImageConfigKey] = dt
} }
if dt, ok := rr.Metadata["containerimage.buildinfo/"+platform]; ok { if dt, ok := rr.Metadata["containerimage.buildinfo"]; ok {
metadata["containerimage.buildinfo"] = dt metadata["containerimage.buildinfo"] = dt
} }
if len(metadata) > 0 { if len(metadata) > 0 {
@ -835,32 +864,9 @@ func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *c
if err != nil { if err != nil {
return err return err
} }
so.FrontendAttrs["input-metadata:"+k+"::"+platform] = string(dt) so.FrontendAttrs["input-metadata:"+k] = string(dt)
} }
} }
delete(so.FrontendAttrs, v)
}
if rr.Ref != nil {
st, err := rr.Ref.ToState()
if err != nil {
return err
}
so.FrontendInputs[k] = st
so.FrontendAttrs[v] = "input:" + k
metadata := make(map[string][]byte)
if dt, ok := rr.Metadata[exptypes.ExporterImageConfigKey]; ok {
metadata[exptypes.ExporterImageConfigKey] = dt
}
if dt, ok := rr.Metadata["containerimage.buildinfo"]; ok {
metadata["containerimage.buildinfo"] = dt
}
if len(metadata) > 0 {
dt, err := json.Marshal(metadata)
if err != nil {
return err
}
so.FrontendAttrs["input-metadata:"+k] = string(dt)
}
} }
} }
return nil return nil

View File

@ -3,6 +3,7 @@ package build
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/containerd/containerd/platforms" "github.com/containerd/containerd/platforms"
"github.com/docker/buildx/builder" "github.com/docker/buildx/builder"
@ -46,10 +47,22 @@ func (dp resolvedNode) BuildOpts(ctx context.Context) (gateway.BuildOpts, error)
type matchMaker func(specs.Platform) platforms.MatchComparer type matchMaker func(specs.Platform) platforms.MatchComparer
type cachedGroup[T any] struct {
g flightcontrol.Group[T]
cache map[int]T
cacheMu sync.Mutex
}
func newCachedGroup[T any]() cachedGroup[T] {
return cachedGroup[T]{
cache: map[int]T{},
}
}
type nodeResolver struct { type nodeResolver struct {
nodes []builder.Node nodes []builder.Node
clients flightcontrol.Group[*client.Client] clients cachedGroup[*client.Client]
opt flightcontrol.Group[gateway.BuildOpts] buildOpts cachedGroup[gateway.BuildOpts]
} }
func resolveDrivers(ctx context.Context, nodes []builder.Node, opt map[string]Options, pw progress.Writer) (map[string][]*resolvedNode, error) { func resolveDrivers(ctx context.Context, nodes []builder.Node, opt map[string]Options, pw progress.Writer) (map[string][]*resolvedNode, error) {
@ -63,7 +76,9 @@ func resolveDrivers(ctx context.Context, nodes []builder.Node, opt map[string]Op
func newDriverResolver(nodes []builder.Node) *nodeResolver { func newDriverResolver(nodes []builder.Node) *nodeResolver {
r := &nodeResolver{ r := &nodeResolver{
nodes: nodes, nodes: nodes,
clients: newCachedGroup[*client.Client](),
buildOpts: newCachedGroup[gateway.BuildOpts](),
} }
return r return r
} }
@ -179,6 +194,7 @@ func (r *nodeResolver) resolve(ctx context.Context, ps []specs.Platform, pw prog
resolver: r, resolver: r,
driverIndex: 0, driverIndex: 0,
}) })
nodeIdxs = append(nodeIdxs, 0)
} else { } else {
for i, idx := range nodeIdxs { for i, idx := range nodeIdxs {
node := &resolvedNode{ node := &resolvedNode{
@ -237,11 +253,24 @@ func (r *nodeResolver) boot(ctx context.Context, idxs []int, pw progress.Writer)
for i, idx := range idxs { for i, idx := range idxs {
i, idx := i, idx i, idx := i, idx
eg.Go(func() error { eg.Go(func() error {
c, err := r.clients.Do(ctx, fmt.Sprint(idx), func(ctx context.Context) (*client.Client, error) { c, err := r.clients.g.Do(ctx, fmt.Sprint(idx), func(ctx context.Context) (*client.Client, error) {
if r.nodes[idx].Driver == nil { if r.nodes[idx].Driver == nil {
return nil, nil return nil, nil
} }
return driver.Boot(ctx, baseCtx, r.nodes[idx].Driver, pw) r.clients.cacheMu.Lock()
c, ok := r.clients.cache[idx]
r.clients.cacheMu.Unlock()
if ok {
return c, nil
}
c, err := driver.Boot(ctx, baseCtx, r.nodes[idx].Driver, pw)
if err != nil {
return nil, err
}
r.clients.cacheMu.Lock()
r.clients.cache[idx] = c
r.clients.cacheMu.Unlock()
return c, nil
}) })
if err != nil { if err != nil {
return err return err
@ -272,14 +301,25 @@ func (r *nodeResolver) opts(ctx context.Context, idxs []int, pw progress.Writer)
continue continue
} }
eg.Go(func() error { eg.Go(func() error {
opt, err := r.opt.Do(ctx, fmt.Sprint(idx), func(ctx context.Context) (gateway.BuildOpts, error) { opt, err := r.buildOpts.g.Do(ctx, fmt.Sprint(idx), func(ctx context.Context) (gateway.BuildOpts, error) {
opt := gateway.BuildOpts{} r.buildOpts.cacheMu.Lock()
opt, ok := r.buildOpts.cache[idx]
r.buildOpts.cacheMu.Unlock()
if ok {
return opt, nil
}
_, err := c.Build(ctx, client.SolveOpt{ _, err := c.Build(ctx, client.SolveOpt{
Internal: true, Internal: true,
}, "buildx", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { }, "buildx", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
opt = c.BuildOpts() opt = c.BuildOpts()
return nil, nil return nil, nil
}, nil) }, nil)
if err != nil {
return gateway.BuildOpts{}, err
}
r.buildOpts.cacheMu.Lock()
r.buildOpts.cache[idx] = opt
r.buildOpts.cacheMu.Unlock()
return opt, err return opt, err
}) })
if err != nil { if err != nil {

View File

@ -162,7 +162,7 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt Op
case 1: case 1:
// valid // valid
case 0: case 0:
if !noDefaultLoad() { if !noDefaultLoad() && opt.PrintFunc == nil {
if nodeDriver.IsMobyDriver() { if nodeDriver.IsMobyDriver() {
// backwards compat for docker driver only: // backwards compat for docker driver only:
// this ensures the build results in a docker image. // this ensures the build results in a docker image.

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
@ -15,6 +16,7 @@ import (
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/util/stack" "github.com/moby/buildkit/util/stack"
"go.opentelemetry.io/otel"
//nolint:staticcheck // vendored dependencies may still use this //nolint:staticcheck // vendored dependencies may still use this
"github.com/containerd/containerd/pkg/seed" "github.com/containerd/containerd/pkg/seed"
@ -38,10 +40,27 @@ func runStandalone(cmd *command.DockerCli) error {
if err := cmd.Initialize(cliflags.NewClientOptions()); err != nil { if err := cmd.Initialize(cliflags.NewClientOptions()); err != nil {
return err return err
} }
defer flushMetrics(cmd)
rootCmd := commands.NewRootCmd(os.Args[0], false, cmd) rootCmd := commands.NewRootCmd(os.Args[0], false, cmd)
return rootCmd.Execute() 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 { func runPlugin(cmd *command.DockerCli) error {
rootCmd := commands.NewRootCmd("buildx", true, cmd) rootCmd := commands.NewRootCmd("buildx", true, cmd)
return plugin.RunPlugin(cmd, rootCmd, manager.Metadata{ return plugin.RunPlugin(cmd, rootCmd, manager.Metadata{

View File

@ -122,27 +122,26 @@ func (o *buildOptions) toControllerOptions() (*controllerapi.BuildOptions, error
} }
opts := controllerapi.BuildOptions{ opts := controllerapi.BuildOptions{
Allow: o.allow, Allow: o.allow,
Annotations: o.annotations, Annotations: o.annotations,
BuildArgs: buildArgs, BuildArgs: buildArgs,
CgroupParent: o.cgroupParent, CgroupParent: o.cgroupParent,
ContextPath: o.contextPath, ContextPath: o.contextPath,
DockerfileName: o.dockerfileName, DockerfileName: o.dockerfileName,
ExtraHosts: o.extraHosts, ExtraHosts: o.extraHosts,
Labels: labels, Labels: labels,
NetworkMode: o.networkMode, NetworkMode: o.networkMode,
NoCacheFilter: o.noCacheFilter, NoCacheFilter: o.noCacheFilter,
Platforms: o.platforms, Platforms: o.platforms,
ShmSize: int64(o.shmSize), ShmSize: int64(o.shmSize),
Tags: o.tags, Tags: o.tags,
Target: o.target, Target: o.target,
Ulimits: dockerUlimitToControllerUlimit(o.ulimits), Ulimits: dockerUlimitToControllerUlimit(o.ulimits),
Builder: o.builder, Builder: o.builder,
NoCache: o.noCache, NoCache: o.noCache,
Pull: o.pull, Pull: o.pull,
ExportPush: o.exportPush, ExportPush: o.exportPush,
ExportLoad: o.exportLoad, ExportLoad: o.exportLoad,
WithProvenanceResponse: len(o.metadataFile) > 0,
} }
// TODO: extract env var parsing to a method easily usable by library consumers // TODO: extract env var parsing to a method easily usable by library consumers
@ -207,6 +206,8 @@ func (o *buildOptions) toControllerOptions() (*controllerapi.BuildOptions, error
return nil, err return nil, err
} }
opts.WithProvenanceResponse = opts.PrintFunc == nil && len(o.metadataFile) > 0
return &opts, nil return &opts, nil
} }
@ -268,8 +269,7 @@ func (o *buildOptionsHash) String() string {
} }
func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) (err error) { func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) (err error) {
mp := dockerCli.MeterProvider(ctx) mp := dockerCli.MeterProvider()
defer metricutil.Shutdown(ctx, mp)
ctx, end, err := tracing.TraceCurrentCommand(ctx, "build") ctx, end, err := tracing.TraceCurrentCommand(ctx, "build")
if err != nil { if err != nil {
@ -365,15 +365,14 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
return errors.Wrap(err, "writing image ID file") return errors.Wrap(err, "writing image ID file")
} }
} }
if options.metadataFile != "" {
if err := writeMetadataFile(options.metadataFile, decodeExporterResponse(resp.ExporterResponse)); err != nil {
return err
}
}
if opts.PrintFunc != nil { if opts.PrintFunc != nil {
if err := printResult(opts.PrintFunc, resp.ExporterResponse); err != nil { if err := printResult(opts.PrintFunc, resp.ExporterResponse); err != nil {
return err return err
} }
} else if options.metadataFile != "" {
if err := writeMetadataFile(options.metadataFile, decodeExporterResponse(resp.ExporterResponse)); err != nil {
return err
}
} }
return nil return nil
} }

View File

@ -83,6 +83,11 @@ func ParseBuilderName(name string) (string, error) {
func Boot(ctx, clientContext context.Context, d *DriverHandle, pw progress.Writer) (*client.Client, error) { func Boot(ctx, clientContext context.Context, d *DriverHandle, pw progress.Writer) (*client.Client, error) {
try := 0 try := 0
logger := discardLogger
if pw != nil {
logger = pw.Write
}
for { for {
info, err := d.Info(ctx) info, err := d.Info(ctx)
if err != nil { if err != nil {
@ -93,7 +98,7 @@ func Boot(ctx, clientContext context.Context, d *DriverHandle, pw progress.Write
if try > 2 { if try > 2 {
return nil, errors.Errorf("failed to bootstrap %T driver in attempts", d) return nil, errors.Errorf("failed to bootstrap %T driver in attempts", d)
} }
if err := d.Bootstrap(ctx, pw.Write); err != nil { if err := d.Bootstrap(ctx, logger); err != nil {
return nil, err return nil, err
} }
} }
@ -109,6 +114,8 @@ func Boot(ctx, clientContext context.Context, d *DriverHandle, pw progress.Write
} }
} }
func discardLogger(*client.SolveStatus) {}
func historyAPISupported(ctx context.Context, c *client.Client) bool { func historyAPISupported(ctx context.Context, c *client.Client) bool {
cl, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{ cl, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
ActiveOnly: true, ActiveOnly: true,

2
go.mod
View File

@ -14,7 +14,7 @@ require (
github.com/containerd/typeurl/v2 v2.1.1 github.com/containerd/typeurl/v2 v2.1.1
github.com/creack/pty v1.1.18 github.com/creack/pty v1.1.18
github.com/distribution/reference v0.5.0 github.com/distribution/reference v0.5.0
github.com/docker/cli v26.0.1-0.20240410153731-b6c552212837+incompatible // v26.1.0-dev github.com/docker/cli v26.1.3+incompatible
github.com/docker/cli-docs-tool v0.7.0 github.com/docker/cli-docs-tool v0.7.0
github.com/docker/docker v26.0.0+incompatible github.com/docker/docker v26.0.0+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0

4
go.sum
View File

@ -117,8 +117,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v26.0.1-0.20240410153731-b6c552212837+incompatible h1:KTmSJjZSQM+cpaczHecGsBNlgJtRccef/62pCOeiA9o= github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc=
github.com/docker/cli v26.0.1-0.20240410153731-b6c552212837+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.7.0 h1:M2Da98Unz2kz3A5d4yeSGbhyOge2mfYSNjAFt01Rw0M= github.com/docker/cli-docs-tool v0.7.0 h1:M2Da98Unz2kz3A5d4yeSGbhyOge2mfYSNjAFt01Rw0M=
github.com/docker/cli-docs-tool v0.7.0/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o= github.com/docker/cli-docs-tool v0.7.0/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=

View File

@ -1,11 +1,7 @@
package metricutil package metricutil
import ( import (
"context"
"github.com/docker/buildx/version" "github.com/docker/buildx/version"
"github.com/docker/cli/cli/command"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
) )
@ -15,10 +11,3 @@ func Meter(mp metric.MeterProvider) metric.Meter {
return mp.Meter(version.Package, return mp.Meter(version.Package,
metric.WithInstrumentationVersion(version.Version)) metric.WithInstrumentationVersion(version.Version))
} }
// Shutdown invokes Shutdown on the MeterProvider and then reports any error to the OTEL handler.
func Shutdown(ctx context.Context, mp command.MeterProvider) {
if err := mp.Shutdown(ctx); err != nil {
otel.Handle(err)
}
}

View File

@ -15,7 +15,7 @@ type Writer interface {
ClearLogSource(interface{}) ClearLogSource(interface{})
} }
func Write(w Writer, name string, f func() error) { func Write(w Writer, name string, f func() error) error {
dgst := digest.FromBytes([]byte(identity.NewID())) dgst := digest.FromBytes([]byte(identity.NewID()))
tm := time.Now() tm := time.Now()
@ -40,6 +40,8 @@ func Write(w Writer, name string, f func() error) {
w.Write(&client.SolveStatus{ w.Write(&client.SolveStatus{
Vertexes: []*client.Vertex{&vtx2}, Vertexes: []*client.Vertex{&vtx2},
}) })
return err
} }
func WriteBuildRef(w Writer, target string, ref string) { func WriteBuildRef(w Writer, target string, ref string) {

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"text/template" "text/template"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -71,18 +72,18 @@ func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i)) return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
} }
func ParseTemplate(hookTemplate string, cmd *cobra.Command) (string, error) { func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
tmpl := template.New("").Funcs(commandFunctions) tmpl := template.New("").Funcs(commandFunctions)
tmpl, err := tmpl.Parse(hookTemplate) tmpl, err := tmpl.Parse(hookTemplate)
if err != nil { if err != nil {
return "", err return nil, err
} }
b := bytes.Buffer{} b := bytes.Buffer{}
err = tmpl.Execute(&b, cmd) err = tmpl.Execute(&b, cmd)
if err != nil { if err != nil {
return "", err return nil, err
} }
return b.String(), nil return strings.Split(b.String(), "\n"), nil
} }
var ErrHookTemplateParse = errors.New("failed to parse hook template") var ErrHookTemplateParse = errors.New("failed to parse hook template")

View File

@ -14,31 +14,42 @@ import (
// that plugins declaring support for hooks get passed when // that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution. // being invoked following a CLI command execution.
type HookPluginData struct { type HookPluginData struct {
RootCmd string // RootCmd is a string representing the matching hook configuration
Flags map[string]string // which is currently being invoked. If a hook for `docker context` is
// configured and the user executes `docker context ls`, the plugin will
// be invoked with `context`.
RootCmd string
Flags map[string]string
CommandError string
} }
// RunPluginHooks calls the hook subcommand for all present // RunCLICommandHooks is the entrypoint into the hooks execution flow after
// CLI plugins that declare support for hooks in their metadata // a main CLI command was executed. It calls the hook subcommand for all
// and parses/prints their responses. // present CLI plugins that declare support for hooks in their metadata and
func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, plugin string, args []string) error { // parses/prints their responses.
subCmdName := subCommand.Name() func RunCLICommandHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
if plugin != "" { commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
subCmdName = plugin flags := getCommandFlags(subCommand)
}
var flags map[string]string runHooks(dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
if plugin == "" { }
flags = getCommandFlags(subCommand)
} else { // RunPluginHooks is the entrypoint for the hooks execution flow
flags = getNaiveFlags(args) // after a plugin command was just executed by the CLI.
} func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, subCmdName, flags) commandName := strings.Join(args, " ")
flags := getNaiveFlags(args)
runHooks(dockerCli, rootCmd, subCommand, commandName, flags, "")
}
func runHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintNextSteps(dockerCli.Err(), nextSteps) hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
return nil
} }
func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, hookCmdName string, flags map[string]string) []string { func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
pluginsCfg := dockerCli.ConfigFile().Plugins pluginsCfg := dockerCli.ConfigFile().Plugins
if pluginsCfg == nil { if pluginsCfg == nil {
return nil return nil
@ -46,7 +57,8 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command
nextSteps := make([]string, 0, len(pluginsCfg)) nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, cfg := range pluginsCfg { for pluginName, cfg := range pluginsCfg {
if !registersHook(cfg, hookCmdName) { match, ok := pluginMatch(cfg, subCmdStr)
if !ok {
continue continue
} }
@ -55,7 +67,11 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command
continue continue
} }
hookReturn, err := p.RunHook(hookCmdName, flags) hookReturn, err := p.RunHook(HookPluginData{
RootCmd: match,
Flags: flags,
CommandError: cmdErrorMessage,
})
if err != nil { if err != nil {
// skip misbehaving plugins, but don't halt execution // skip misbehaving plugins, but don't halt execution
continue continue
@ -76,23 +92,46 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command
if err != nil { if err != nil {
continue continue
} }
nextSteps = append(nextSteps, processedHook) nextSteps = append(nextSteps, processedHook...)
} }
return nextSteps return nextSteps
} }
func registersHook(pluginCfg map[string]string, subCmdName string) bool { // pluginMatch takes a plugin configuration and a string representing the
hookCmdStr, ok := pluginCfg["hooks"] // command being executed (such as 'image ls' the root 'docker' is omitted)
if !ok { // and, if the configuration includes a hook for the invoked command, returns
return false // the configured hook string.
func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
configuredPluginHooks, ok := pluginCfg["hooks"]
if !ok || configuredPluginHooks == "" {
return "", false
} }
commands := strings.Split(hookCmdStr, ",")
commands := strings.Split(configuredPluginHooks, ",")
for _, hookCmd := range commands { for _, hookCmd := range commands {
if hookCmd == subCmdName { if hookMatch(hookCmd, subCmd) {
return true return hookCmd, true
} }
} }
return false
return "", false
}
func hookMatch(hookCmd, subCmd string) bool {
hookCmdTokens := strings.Split(hookCmd, " ")
subCmdTokens := strings.Split(subCmd, " ")
if len(hookCmdTokens) > len(subCmdTokens) {
return false
}
for i, v := range hookCmdTokens {
if v != subCmdTokens[i] {
return false
}
}
return true
} }
func getCommandFlags(cmd *cobra.Command) map[string]string { func getCommandFlags(cmd *cobra.Command) map[string]string {

View File

@ -240,8 +240,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = os.Environ() cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin) cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
return cmd, nil return cmd, nil

View File

@ -2,6 +2,7 @@ package manager
import ( import (
"encoding/json" "encoding/json"
"os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -104,16 +105,16 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
// RunHook executes the plugin's hooks command // RunHook executes the plugin's hooks command
// and returns its unprocessed output. // and returns its unprocessed output.
func (p *Plugin) RunHook(cmdName string, flags map[string]string) ([]byte, error) { func (p *Plugin) RunHook(hookData HookPluginData) ([]byte, error) {
hDataBytes, err := json.Marshal(HookPluginData{ hDataBytes, err := json.Marshal(hookData)
RootCmd: cmdName,
Flags: flags,
})
if err != nil { if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data") return nil, wrapAsPluginError(err, "failed to marshall hook data")
} }
hookCmdOutput, err := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes)).Output() pCmd := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes))
pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output()
if err != nil { if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand") return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
} }

View File

@ -52,6 +52,24 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
opts = append(opts, withPluginClientConn(plugin.Name())) opts = append(opts, withPluginClientConn(plugin.Name()))
} }
err = tcmd.Initialize(opts...) err = tcmd.Initialize(opts...)
ogRunE := cmd.RunE
if ogRunE == nil {
ogRun := cmd.Run
// necessary because error will always be nil here
// see: https://github.com/golangci/golangci-lint/issues/1379
//nolint:unparam
ogRunE = func(cmd *cobra.Command, args []string) error {
ogRun(cmd, args)
return nil
}
cmd.Run = nil
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
stopInstrumentation := dockerCli.StartInstrumentation(cmd)
err := ogRunE(cmd, args)
stopInstrumentation(err)
return err
}
}) })
return err return err
} }

View File

@ -273,6 +273,11 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
return ResolveDefaultContext(cli.options, cli.contextStoreConfig) return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
}, },
} }
// TODO(krissetto): pass ctx to the funcs instead of using this
cli.createGlobalMeterProvider(cli.baseCtx)
cli.createGlobalTracerProvider(cli.baseCtx)
return nil return nil
} }

View File

@ -41,35 +41,25 @@ type TelemetryClient interface {
// each time this function is invoked. // each time this function is invoked.
Resource() *resource.Resource Resource() *resource.Resource
// TracerProvider returns a TracerProvider. This TracerProvider will be configured // TracerProvider returns the currently initialized TracerProvider. This TracerProvider will be configured
// with the default tracing components for a CLI program along with any options given // with the default tracing components for a CLI program
// for the SDK. TracerProvider() trace.TracerProvider
TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider
// MeterProvider returns a MeterProvider. This MeterProvider will be configured // MeterProvider returns the currently initialized MeterProvider. This MeterProvider will be configured
// with the default metric components for a CLI program along with any options given // with the default metric components for a CLI program
// for the SDK. MeterProvider() metric.MeterProvider
MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider
} }
func (cli *DockerCli) Resource() *resource.Resource { func (cli *DockerCli) Resource() *resource.Resource {
return cli.res.Get() return cli.res.Get()
} }
func (cli *DockerCli) TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider { func (cli *DockerCli) TracerProvider() trace.TracerProvider {
allOpts := make([]sdktrace.TracerProviderOption, 0, len(opts)+2) return otel.GetTracerProvider()
allOpts = append(allOpts, sdktrace.WithResource(cli.Resource()))
allOpts = append(allOpts, dockerSpanExporter(ctx, cli)...)
allOpts = append(allOpts, opts...)
return sdktrace.NewTracerProvider(allOpts...)
} }
func (cli *DockerCli) MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider { func (cli *DockerCli) MeterProvider() metric.MeterProvider {
allOpts := make([]sdkmetric.Option, 0, len(opts)+2) return otel.GetMeterProvider()
allOpts = append(allOpts, sdkmetric.WithResource(cli.Resource()))
allOpts = append(allOpts, dockerMetricExporter(ctx, cli)...)
allOpts = append(allOpts, opts...)
return sdkmetric.NewMeterProvider(allOpts...)
} }
// WithResourceOptions configures additional options for the default resource. The default // WithResourceOptions configures additional options for the default resource. The default
@ -122,6 +112,28 @@ func (r *telemetryResource) init() {
r.opts = nil r.opts = nil
} }
// createGlobalMeterProvider creates a new MeterProvider from the initialized DockerCli struct
// with the given options and sets it as the global meter provider
func (cli *DockerCli) createGlobalMeterProvider(ctx context.Context, opts ...sdkmetric.Option) {
allOpts := make([]sdkmetric.Option, 0, len(opts)+2)
allOpts = append(allOpts, sdkmetric.WithResource(cli.Resource()))
allOpts = append(allOpts, dockerMetricExporter(ctx, cli)...)
allOpts = append(allOpts, opts...)
mp := sdkmetric.NewMeterProvider(allOpts...)
otel.SetMeterProvider(mp)
}
// createGlobalTracerProvider creates a new TracerProvider from the initialized DockerCli struct
// with the given options and sets it as the global tracer provider
func (cli *DockerCli) createGlobalTracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) {
allOpts := make([]sdktrace.TracerProviderOption, 0, len(opts)+2)
allOpts = append(allOpts, sdktrace.WithResource(cli.Resource()))
allOpts = append(allOpts, dockerSpanExporter(ctx, cli)...)
allOpts = append(allOpts, opts...)
tp := sdktrace.NewTracerProvider(allOpts...)
otel.SetTracerProvider(tp)
}
func defaultResourceOptions() []resource.Option { func defaultResourceOptions() []resource.Option {
return []resource.Option{ return []resource.Option{
resource.WithDetectors(serviceNameDetector{}), resource.WithDetectors(serviceNameDetector{}),
@ -174,11 +186,6 @@ func newCLIReader(exp sdkmetric.Exporter) sdkmetric.Reader {
} }
func (r *cliReader) Shutdown(ctx context.Context) error { func (r *cliReader) Shutdown(ctx context.Context) error {
var rm metricdata.ResourceMetrics
if err := r.Reader.Collect(ctx, &rm); err != nil {
return err
}
// Place a pretty tight constraint on the actual reporting. // Place a pretty tight constraint on the actual reporting.
// We don't want CLI metrics to prevent the CLI from exiting // We don't want CLI metrics to prevent the CLI from exiting
// so if there's some kind of issue we need to abort pretty // so if there's some kind of issue we need to abort pretty
@ -186,6 +193,15 @@ func (r *cliReader) Shutdown(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, exportTimeout) ctx, cancel := context.WithTimeout(ctx, exportTimeout)
defer cancel() defer cancel()
return r.ForceFlush(ctx)
}
func (r *cliReader) ForceFlush(ctx context.Context) error {
var rm metricdata.ResourceMetrics
if err := r.Reader.Collect(ctx, &rm); err != nil {
return err
}
return r.exporter.Export(ctx, &rm) return r.exporter.Export(ctx, &rm)
} }

View File

@ -41,24 +41,20 @@ func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) {
otelCfg = m[otelContextFieldName] otelCfg = m[otelContextFieldName]
} }
if otelCfg == nil { if otelCfg != nil {
return "", false otelMap, ok := otelCfg.(map[string]any)
if !ok {
otel.Handle(errors.Errorf(
"unexpected type for field %q: %T (expected: %T)",
otelContextFieldName,
otelCfg,
otelMap,
))
}
// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
endpoint, _ = otelMap[otelExporterOTLPEndpoint].(string)
} }
otelMap, ok := otelCfg.(map[string]any)
if !ok {
otel.Handle(errors.Errorf(
"unexpected type for field %q: %T (expected: %T)",
otelContextFieldName,
otelCfg,
otelMap,
))
return "", false
}
// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
endpoint, _ = otelMap[otelExporterOTLPEndpoint].(string)
// Override with env var value if it exists AND IS SET // Override with env var value if it exists AND IS SET
// (ignore otel defaults for this override when the key exists but is empty) // (ignore otel defaults for this override when the key exists but is empty)
if override := os.Getenv(debugEnvVarPrefix + otelExporterOTLPEndpoint); override != "" { if override := os.Getenv(debugEnvVarPrefix + otelExporterOTLPEndpoint); override != "" {

View File

@ -26,8 +26,7 @@ func BaseCommandAttributes(cmd *cobra.Command, streams Streams) []attribute.KeyV
// Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution. // Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution.
// //
// can also be used for spans! // can also be used for spans!
func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) { func (cli *DockerCli) InstrumentCobraCommands(ctx context.Context, cmd *cobra.Command) {
meter := getDefaultMeter(mp)
// If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default // If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default
ogPersistentPreRunE := cmd.PersistentPreRunE ogPersistentPreRunE := cmd.PersistentPreRunE
if ogPersistentPreRunE == nil { if ogPersistentPreRunE == nil {
@ -55,10 +54,9 @@ func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.Mete
} }
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
// start the timer as the first step of every cobra command // start the timer as the first step of every cobra command
baseAttrs := BaseCommandAttributes(cmd, cli) stopInstrumentation := cli.StartInstrumentation(cmd)
stopCobraCmdTimer := startCobraCommandTimer(cmd, meter, baseAttrs)
cmdErr := ogRunE(cmd, args) cmdErr := ogRunE(cmd, args)
stopCobraCmdTimer(cmdErr) stopInstrumentation(cmdErr)
return cmdErr return cmdErr
} }
@ -66,8 +64,17 @@ func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.Mete
} }
} }
func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attribute.KeyValue) func(err error) { // StartInstrumentation instruments CLI commands with the individual metrics and spans configured.
ctx := cmd.Context() // It's the main command OTel utility, and new command-related metrics should be added to it.
// It should be called immediately before command execution, and returns a stopInstrumentation function
// that must be called with the error resulting from the command execution.
func (cli *DockerCli) StartInstrumentation(cmd *cobra.Command) (stopInstrumentation func(error)) {
baseAttrs := BaseCommandAttributes(cmd, cli)
return startCobraCommandTimer(cli.MeterProvider(), baseAttrs)
}
func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue) func(err error) {
meter := getDefaultMeter(mp)
durationCounter, _ := meter.Float64Counter( durationCounter, _ := meter.Float64Counter(
"command.time", "command.time",
metric.WithDescription("Measures the duration of the cobra command"), metric.WithDescription("Measures the duration of the cobra command"),
@ -76,12 +83,20 @@ func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attr
start := time.Now() start := time.Now()
return func(err error) { return func(err error) {
// Use a new context for the export so that the command being cancelled
// doesn't affect the metrics, and we get metrics for cancelled commands.
ctx, cancel := context.WithTimeout(context.Background(), exportTimeout)
defer cancel()
duration := float64(time.Since(start)) / float64(time.Millisecond) duration := float64(time.Since(start)) / float64(time.Millisecond)
cmdStatusAttrs := attributesFromError(err) cmdStatusAttrs := attributesFromError(err)
durationCounter.Add(ctx, duration, durationCounter.Add(ctx, duration,
metric.WithAttributes(attrs...), metric.WithAttributes(attrs...),
metric.WithAttributes(cmdStatusAttrs...), metric.WithAttributes(cmdStatusAttrs...),
) )
if mp, ok := mp.(MeterProvider); ok {
mp.ForceFlush(ctx)
}
} }
} }

2
vendor/modules.txt vendored
View File

@ -215,7 +215,7 @@ github.com/davecgh/go-spew/spew
# github.com/distribution/reference v0.5.0 # github.com/distribution/reference v0.5.0
## explicit; go 1.20 ## explicit; go 1.20
github.com/distribution/reference github.com/distribution/reference
# github.com/docker/cli v26.0.1-0.20240410153731-b6c552212837+incompatible # github.com/docker/cli v26.1.3+incompatible
## explicit ## explicit
github.com/docker/cli/cli github.com/docker/cli/cli
github.com/docker/cli/cli-plugins/hooks github.com/docker/cli/cli-plugins/hooks