From 85dcacd78f2a45886b1ab86ef49fe17014852add Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Tue, 13 Feb 2024 15:40:55 -0600 Subject: [PATCH 1/2] plugins: set OTEL_RESOURCE_ATTRIBUTES when invoking a plugin When a plugin is invoked, the docker cli will now set `OTEL_RESOURCE_ATTRIBUTES` to pass OTEL resource attribute names to the plugin as additional resource attributes. At the moment, the only resource attribute passed is `cobra.command_path`. All resource attributes passed by the CLI are prepended with the namespace `docker.cli` to avoid clashing with existing ones the plugin uses or ones defined by the user. For aliased commands like the various builder commands, the command path is overwritten to match with the original name (such as `docker builder`) instead of the forwarded name (such as `docker buildx build`). Signed-off-by: Jonathan A. Sternberg --- cli-plugins/manager/cobra.go | 40 ++++++++++++++++++++++++++++++++++ cli-plugins/manager/manager.go | 17 ++++++++++----- cmd/docker/builder.go | 11 ++++++---- vendor.mod | 2 +- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/cli-plugins/manager/cobra.go b/cli-plugins/manager/cobra.go index be3a058201..219c74ee67 100644 --- a/cli-plugins/manager/cobra.go +++ b/cli-plugins/manager/cobra.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/spf13/cobra" + "go.opentelemetry.io/otel/attribute" ) const ( @@ -30,6 +31,10 @@ const ( // is, one which failed it's candidate test) and contains the // reason for the failure. CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid" + + // CommandAnnotationPluginCommandPath is added to overwrite the + // command path for a plugin invocation. + CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path" ) var pluginCommandStubsOnce sync.Once @@ -98,3 +103,38 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e }) return err } + +const ( + dockerCliAttributePrefix = attribute.Key("docker.cli") + + cobraCommandPath = attribute.Key("cobra.command_path") +) + +func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set { + commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath] + if commandPath == "" { + commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name) + } + + attrSet := attribute.NewSet( + cobraCommandPath.String(commandPath), + ) + + kvs := make([]attribute.KeyValue, 0, attrSet.Len()) + for iter := attrSet.Iter(); iter.Next(); { + attr := iter.Attribute() + kvs = append(kvs, attribute.KeyValue{ + Key: dockerCliAttributePrefix + "." + attr.Key, + Value: attr.Value, + }) + } + return attribute.NewSet(kvs...) +} + +func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string { + if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 { + envVarVal := attrs.Encoded(attribute.DefaultEncoder()) + env = append(env, ResourceAttributesEnvvar+"="+envVarVal) + } + return env +} diff --git a/cli-plugins/manager/manager.go b/cli-plugins/manager/manager.go index d23934940d..d3b7b7b526 100644 --- a/cli-plugins/manager/manager.go +++ b/cli-plugins/manager/manager.go @@ -17,11 +17,17 @@ import ( "golang.org/x/sync/errgroup" ) -// ReexecEnvvar is the name of an ennvar which is set to the command -// used to originally invoke the docker CLI when executing a -// plugin. Assuming $PATH and $CWD remain unchanged this should allow -// the plugin to re-execute the original CLI. -const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND" +const ( + // ReexecEnvvar is the name of an ennvar which is set to the command + // used to originally invoke the docker CLI when executing a + // plugin. Assuming $PATH and $CWD remain unchanged this should allow + // the plugin to re-execute the original CLI. + ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND" + + // ResourceAttributesEnvvar is the name of the envvar that includes additional + // resource attributes for OTEL. + ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES" +) // errPluginNotFound is the error returned when a plugin could not be found. type errPluginNotFound string @@ -236,6 +242,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command cmd.Env = os.Environ() cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0]) + cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin) return cmd, nil } diff --git a/cmd/docker/builder.go b/cmd/docker/builder.go index 7807f80b1a..ad28d655cc 100644 --- a/cmd/docker/builder.go +++ b/cmd/docker/builder.go @@ -69,7 +69,7 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st } // is this a build that should be forwarded to the builder? - fwargs, fwosargs, forwarded := forwardBuilder(builderAlias, args, osargs) + fwargs, fwosargs, alias, forwarded := forwardBuilder(builderAlias, args, osargs) if !forwarded { return args, osargs, nil, nil } @@ -116,10 +116,13 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st envs = append([]string{"BUILDX_BUILDER=" + dockerCli.CurrentContext()}, envs...) } + // overwrite the command path for this plugin using the alias name. + cmd.Annotations[pluginmanager.CommandAnnotationPluginCommandPath] = fmt.Sprintf("%s %s", cmd.CommandPath(), alias) + return fwargs, fwosargs, envs, nil } -func forwardBuilder(alias string, args, osargs []string) ([]string, []string, bool) { +func forwardBuilder(alias string, args, osargs []string) ([]string, []string, string, bool) { aliases := [][2][]string{ { {"builder"}, @@ -137,10 +140,10 @@ func forwardBuilder(alias string, args, osargs []string) ([]string, []string, bo for _, al := range aliases { if fwargs, changed := command.StringSliceReplaceAt(args, al[0], al[1], 0); changed { fwosargs, _ := command.StringSliceReplaceAt(osargs, al[0], al[1], -1) - return fwargs, fwosargs, true + return fwargs, fwosargs, al[0][0], true } } - return args, osargs, false + return args, osargs, "", false } // hasBuilderName checks if a builder name is defined in args or env vars diff --git a/vendor.mod b/vendor.mod index fa684ca784..33ed56e342 100644 --- a/vendor.mod +++ b/vendor.mod @@ -37,6 +37,7 @@ require ( github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d github.com/xeipuuv/gojsonschema v1.2.0 + go.opentelemetry.io/otel v1.21.0 golang.org/x/sync v0.6.0 golang.org/x/sys v0.16.0 golang.org/x/term v0.15.0 @@ -77,7 +78,6 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect go.etcd.io/etcd/raft/v3 v3.5.6 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect golang.org/x/crypto v0.17.0 // indirect From 5786f20687f334146a229f0954e968c8b43ab92a Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Wed, 28 Feb 2024 12:36:45 -0800 Subject: [PATCH 2/2] plugins: fix encoding for OTEL env var passed to plugin Signed-off-by: Tonis Tiigi --- cli-plugins/manager/cobra.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cli-plugins/manager/cobra.go b/cli-plugins/manager/cobra.go index 219c74ee67..feff8a8fd6 100644 --- a/cli-plugins/manager/cobra.go +++ b/cli-plugins/manager/cobra.go @@ -2,7 +2,9 @@ package manager import ( "fmt" + "net/url" "os" + "strings" "sync" "github.com/docker/cli/cli/command" @@ -133,8 +135,14 @@ func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Se func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string { if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 { - envVarVal := attrs.Encoded(attribute.DefaultEncoder()) - env = append(env, ResourceAttributesEnvvar+"="+envVarVal) + // values in environment variables need to be in baggage format + // otel/baggage package can be used after update to v1.22, currently it encodes incorrectly + attrsSlice := make([]string, attrs.Len()) + for iter := attrs.Iter(); iter.Next(); { + i, v := iter.IndexedAttribute() + attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString()) + } + env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ",")) } return env }