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 <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg 2024-02-13 15:40:55 -06:00 committed by Tonis Tiigi
parent eb306df13d
commit 85dcacd78f
No known key found for this signature in database
GPG Key ID: AFA9DE5F8AB7AF39
4 changed files with 60 additions and 10 deletions

View File

@ -7,6 +7,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute"
) )
const ( const (
@ -30,6 +31,10 @@ const (
// is, one which failed it's candidate test) and contains the // is, one which failed it's candidate test) and contains the
// reason for the failure. // reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid" 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 var pluginCommandStubsOnce sync.Once
@ -98,3 +103,38 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
}) })
return err 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
}

View File

@ -17,11 +17,17 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
const (
// ReexecEnvvar is the name of an ennvar which is set to the command // ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a // used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow // plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI. // the plugin to re-execute the original CLI.
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND" 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. // errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string type errPluginNotFound string
@ -236,6 +242,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
cmd.Env = os.Environ() cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0]) cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
return cmd, nil return cmd, nil
} }

View File

@ -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? // 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 { if !forwarded {
return args, osargs, nil, nil 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...) 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 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{ aliases := [][2][]string{
{ {
{"builder"}, {"builder"},
@ -137,10 +140,10 @@ func forwardBuilder(alias string, args, osargs []string) ([]string, []string, bo
for _, al := range aliases { for _, al := range aliases {
if fwargs, changed := command.StringSliceReplaceAt(args, al[0], al[1], 0); changed { if fwargs, changed := command.StringSliceReplaceAt(args, al[0], al[1], 0); changed {
fwosargs, _ := command.StringSliceReplaceAt(osargs, al[0], al[1], -1) 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 // hasBuilderName checks if a builder name is defined in args or env vars

View File

@ -37,6 +37,7 @@ require (
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
go.opentelemetry.io/otel v1.21.0
golang.org/x/sync v0.6.0 golang.org/x/sync v0.6.0
golang.org/x/sys v0.16.0 golang.org/x/sys v0.16.0
golang.org/x/term v0.15.0 golang.org/x/term v0.15.0
@ -77,7 +78,6 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
go.etcd.io/etcd/raft/v3 v3.5.6 // 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/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/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect
golang.org/x/crypto v0.17.0 // indirect golang.org/x/crypto v0.17.0 // indirect