mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 17:37:46 +08:00
metricutil: switch to using the cli meter provider
The meter provider initialization that was located here has now been moved to a common area in the docker cli. This upgrades our CLI version and then uses this common code instead of our own version. As a piece of additional functionality, the docker OTEL endpoint can now be overwritten with `DOCKER_CLI_OTEL_EXPORTER_OTLP_ENDPOINT` for testing. This removes the OTLP exporter from the CLI that was previously locked behind `BUILDX_EXPERIMENTAL`. I do plan for this to return, but as a proper part of the `docker/cli` implementation rather than something special with `buildx`. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
parent
7dc5639216
commit
b4799f9d16
@ -266,11 +266,8 @@ 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, err := metricutil.NewMeterProvider(ctx, dockerCli)
|
mp := dockerCli.MeterProvider(ctx)
|
||||||
if err != nil {
|
defer metricutil.Shutdown(ctx, mp)
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer mp.Report(context.Background())
|
|
||||||
|
|
||||||
ctx, end, err := tracing.TraceCurrentCommand(ctx, "build")
|
ctx, end, err := tracing.TraceCurrentCommand(ctx, "build")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
10
go.mod
10
go.mod
@ -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.0+incompatible
|
github.com/docker/cli v26.0.1-0.20240401150816-155dc5e4e406+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-rc1+incompatible
|
github.com/docker/docker v26.0.0-rc1+incompatible
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
@ -42,11 +42,11 @@ require (
|
|||||||
github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5
|
github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5
|
||||||
github.com/zclconf/go-cty v1.14.1
|
github.com/zclconf/go-cty v1.14.1
|
||||||
go.opentelemetry.io/otel v1.21.0
|
go.opentelemetry.io/otel v1.21.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.21.0
|
go.opentelemetry.io/otel/metric v1.21.0
|
||||||
go.opentelemetry.io/otel/sdk v1.21.0
|
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.21.0
|
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.21.0
|
go.opentelemetry.io/otel/trace v1.21.0
|
||||||
golang.org/x/mod v0.14.0
|
golang.org/x/mod v0.14.0
|
||||||
golang.org/x/sync v0.6.0
|
golang.org/x/sync v0.6.0
|
||||||
|
4
go.sum
4
go.sum
@ -121,8 +121,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.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
|
github.com/docker/cli v26.0.1-0.20240401150816-155dc5e4e406+incompatible h1:+BQSfkbpbEDXXfHmFfmbfstUBFjhffsTkfa0iudOFLc=
|
||||||
github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v26.0.1-0.20240401150816-155dc5e4e406+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=
|
||||||
|
@ -2,218 +2,23 @@ package metricutil
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/buildx/util/confutil"
|
|
||||||
"github.com/docker/buildx/version"
|
"github.com/docker/buildx/version"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"go.opentelemetry.io/otel/metric/noop"
|
|
||||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
||||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
otelConfigFieldName = "otel"
|
|
||||||
reportTimeout = 2 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// MeterProvider holds a MeterProvider for metric generation and the configured
|
|
||||||
// exporters for reporting metrics from the CLI.
|
|
||||||
type MeterProvider struct {
|
|
||||||
metric.MeterProvider
|
|
||||||
reader *sdkmetric.ManualReader
|
|
||||||
exporters []sdkmetric.Exporter
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMeterProvider configures a MeterProvider from the CLI context.
|
|
||||||
func NewMeterProvider(ctx context.Context, cli command.Cli) (*MeterProvider, error) {
|
|
||||||
var exps []sdkmetric.Exporter
|
|
||||||
|
|
||||||
if exp, err := dockerOtelExporter(cli); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if exp != nil {
|
|
||||||
exps = append(exps, exp)
|
|
||||||
}
|
|
||||||
|
|
||||||
if confutil.IsExperimental() {
|
|
||||||
// Expose the user-facing metric exporter only if the experimental flag is set.
|
|
||||||
if exp, err := detectOtlpExporter(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if exp != nil {
|
|
||||||
exps = append(exps, exp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(exps) == 0 {
|
|
||||||
// No exporters are configured so use a noop provider.
|
|
||||||
return &MeterProvider{
|
|
||||||
MeterProvider: noop.NewMeterProvider(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := sdkmetric.NewManualReader(
|
|
||||||
sdkmetric.WithTemporalitySelector(deltaTemporality),
|
|
||||||
)
|
|
||||||
mp := sdkmetric.NewMeterProvider(
|
|
||||||
sdkmetric.WithResource(Resource()),
|
|
||||||
sdkmetric.WithReader(reader),
|
|
||||||
)
|
|
||||||
return &MeterProvider{
|
|
||||||
MeterProvider: mp,
|
|
||||||
reader: reader,
|
|
||||||
exporters: exps,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report exports metrics to the configured exporter. This should be done before the CLI
|
|
||||||
// exits.
|
|
||||||
func (m *MeterProvider) Report(ctx context.Context) {
|
|
||||||
if m.reader == nil {
|
|
||||||
// Not configured.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, reportTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var rm metricdata.ResourceMetrics
|
|
||||||
if err := m.reader.Collect(ctx, &rm); err != nil {
|
|
||||||
// Error when collecting metrics. Do not send any.
|
|
||||||
otel.Handle(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var eg errgroup.Group
|
|
||||||
for _, exp := range m.exporters {
|
|
||||||
exp := exp
|
|
||||||
eg.Go(func() error {
|
|
||||||
if err := exp.Export(ctx, &rm); err != nil {
|
|
||||||
otel.Handle(err)
|
|
||||||
}
|
|
||||||
_ = exp.Shutdown(ctx)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can't report an error because we don't allow it to.
|
|
||||||
_ = eg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// dockerOtelExporter reads the CLI metadata to determine an OTLP exporter
|
|
||||||
// endpoint for docker metrics to be sent.
|
|
||||||
//
|
|
||||||
// This location, configuration, and usage is hard-coded as part of
|
|
||||||
// sending usage statistics so this metric reporting is not meant to be
|
|
||||||
// user facing.
|
|
||||||
func dockerOtelExporter(cli command.Cli) (sdkmetric.Exporter, error) {
|
|
||||||
endpoint, err := otelExporterOtlpEndpoint(cli)
|
|
||||||
if endpoint == "" || err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the endpoint. The docker config expects the endpoint to be
|
|
||||||
// in the form of a URL to match the environment variable, but this
|
|
||||||
// option doesn't correspond directly to WithEndpoint.
|
|
||||||
//
|
|
||||||
// We pretend we're the same as the environment reader.
|
|
||||||
u, err := url.Parse(endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("docker otel endpoint is invalid: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var opts []otlpmetricgrpc.Option
|
|
||||||
switch u.Scheme {
|
|
||||||
case "unix":
|
|
||||||
// Unix sockets are a bit weird. OTEL seems to imply they
|
|
||||||
// can be used as an environment variable and are handled properly,
|
|
||||||
// but they don't seem to be as the behavior of the environment variable
|
|
||||||
// is to strip the scheme from the endpoint, but the underlying implementation
|
|
||||||
// needs the scheme to use the correct resolver.
|
|
||||||
//
|
|
||||||
// We'll just handle this in a special way and add the unix:// back to the endpoint.
|
|
||||||
opts = []otlpmetricgrpc.Option{
|
|
||||||
otlpmetricgrpc.WithEndpoint(fmt.Sprintf("unix://%s", path.Join(u.Host, u.Path))),
|
|
||||||
otlpmetricgrpc.WithInsecure(),
|
|
||||||
}
|
|
||||||
case "http":
|
|
||||||
opts = []otlpmetricgrpc.Option{
|
|
||||||
// Omit the scheme when using http or https.
|
|
||||||
otlpmetricgrpc.WithEndpoint(path.Join(u.Host, u.Path)),
|
|
||||||
otlpmetricgrpc.WithInsecure(),
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
opts = []otlpmetricgrpc.Option{
|
|
||||||
// Omit the scheme when using http or https.
|
|
||||||
otlpmetricgrpc.WithEndpoint(path.Join(u.Host, u.Path)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hardcoded endpoint from the endpoint.
|
|
||||||
exp, err := otlpmetricgrpc.New(context.Background(), opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return exp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// otelExporterOtlpEndpoint retrieves the OTLP endpoint used for the docker reporter
|
|
||||||
// from the current context.
|
|
||||||
func otelExporterOtlpEndpoint(cli command.Cli) (string, error) {
|
|
||||||
meta, err := cli.ContextStore().GetMetadata(cli.CurrentContext())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var otelCfg interface{}
|
|
||||||
switch m := meta.Metadata.(type) {
|
|
||||||
case command.DockerContext:
|
|
||||||
otelCfg = m.AdditionalFields[otelConfigFieldName]
|
|
||||||
case map[string]interface{}:
|
|
||||||
otelCfg = m[otelConfigFieldName]
|
|
||||||
}
|
|
||||||
|
|
||||||
if otelCfg == nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
otelMap, ok := otelCfg.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return "", errors.Errorf(
|
|
||||||
"unexpected type for field %q: %T (expected: %T)",
|
|
||||||
otelConfigFieldName,
|
|
||||||
otelCfg,
|
|
||||||
otelMap,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
|
|
||||||
endpoint, _ := otelMap["OTEL_EXPORTER_OTLP_ENDPOINT"].(string)
|
|
||||||
return endpoint, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deltaTemporality sets the Temporality of every instrument to delta.
|
|
||||||
//
|
|
||||||
// This isn't really needed since we create a unique resource on each invocation,
|
|
||||||
// but it can help with cardinality concerns for downstream processors since they can
|
|
||||||
// perform aggregation for a time interval and then discard the data once that time
|
|
||||||
// period has passed. Cumulative temporality would imply to the downstream processor
|
|
||||||
// that they might receive a successive point and they may unnecessarily keep state
|
|
||||||
// they really shouldn't.
|
|
||||||
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
|
|
||||||
return metricdata.DeltaTemporality
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meter returns a Meter from the MetricProvider that indicates the measurement
|
// Meter returns a Meter from the MetricProvider that indicates the measurement
|
||||||
// comes from buildx with the appropriate version.
|
// comes from buildx with the appropriate version.
|
||||||
func Meter(mp metric.MeterProvider) metric.Meter {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
package metricutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
|
||||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
||||||
)
|
|
||||||
|
|
||||||
// detectOtlpExporter configures a metrics exporter based on environment variables.
|
|
||||||
// This is similar to the version of this in buildkit, but we need direct access
|
|
||||||
// to the exporter and the prometheus exporter doesn't work at all in a CLI context.
|
|
||||||
//
|
|
||||||
// There's some duplication here which I hope to remove when the detect package
|
|
||||||
// is refactored or extracted from buildkit so it can be utilized here.
|
|
||||||
//
|
|
||||||
// This version of the exporter is public facing in contrast to the
|
|
||||||
// docker otel collector.
|
|
||||||
func detectOtlpExporter(ctx context.Context) (sdkmetric.Exporter, error) {
|
|
||||||
set := os.Getenv("OTEL_METRICS_EXPORTER") == "otlp" || os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" || os.Getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") != ""
|
|
||||||
if !set {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
proto := os.Getenv("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")
|
|
||||||
if proto == "" {
|
|
||||||
proto = os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL")
|
|
||||||
}
|
|
||||||
if proto == "" {
|
|
||||||
proto = "grpc"
|
|
||||||
}
|
|
||||||
|
|
||||||
switch proto {
|
|
||||||
case "grpc":
|
|
||||||
return otlpmetricgrpc.New(ctx)
|
|
||||||
case "http/protobuf":
|
|
||||||
return otlpmetrichttp.New(ctx)
|
|
||||||
// case "http/json": // unsupported by library
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("unsupported otlp protocol %v", proto)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package metricutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
res *resource.Resource
|
|
||||||
resOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resource retrieves the OTEL resource for the buildx CLI.
|
|
||||||
func Resource() *resource.Resource {
|
|
||||||
resOnce.Do(func() {
|
|
||||||
var err error
|
|
||||||
res, err = resource.New(context.Background(),
|
|
||||||
resource.WithDetectors(serviceNameDetector{}),
|
|
||||||
resource.WithAttributes(
|
|
||||||
// Use a unique instance id so OTEL knows that each invocation
|
|
||||||
// of the CLI is its own instance. Without this, downstream
|
|
||||||
// OTEL processors may think the same process is restarting
|
|
||||||
// continuously and reset the metric counters.
|
|
||||||
semconv.ServiceInstanceID(uuid.New().String()),
|
|
||||||
),
|
|
||||||
resource.WithFromEnv(),
|
|
||||||
resource.WithTelemetrySDK(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
otel.Handle(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
type serviceNameDetector struct{}
|
|
||||||
|
|
||||||
func (serviceNameDetector) Detect(ctx context.Context) (*resource.Resource, error) {
|
|
||||||
return resource.StringDetector(
|
|
||||||
semconv.SchemaURL,
|
|
||||||
semconv.ServiceNameKey,
|
|
||||||
func() (string, error) {
|
|
||||||
return filepath.Base(os.Args[0]), nil
|
|
||||||
},
|
|
||||||
).Detect(ctx)
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package metricutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResource(t *testing.T) {
|
|
||||||
setErrorHandler(t)
|
|
||||||
|
|
||||||
// Ensure resource creation doesn't result in an error.
|
|
||||||
// This is because the schema urls for the various attributes need to be
|
|
||||||
// the same, but it's really easy to import the wrong package when upgrading
|
|
||||||
// otel to anew version and the buildx CLI swallows any visible errors.
|
|
||||||
res := Resource()
|
|
||||||
|
|
||||||
// Ensure an attribute is present.
|
|
||||||
assert.True(t, res.Set().HasValue("telemetry.sdk.version"), "resource attribute missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setErrorHandler(tb testing.TB) {
|
|
||||||
tb.Helper()
|
|
||||||
|
|
||||||
errorHandler := otel.GetErrorHandler()
|
|
||||||
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
|
|
||||||
tb.Errorf("otel error: %s", err)
|
|
||||||
}))
|
|
||||||
tb.Cleanup(func() {
|
|
||||||
otel.SetErrorHandler(errorHandler)
|
|
||||||
})
|
|
||||||
}
|
|
18
vendor/github.com/docker/cli/cli-plugins/hooks/printer.go
generated
vendored
Normal file
18
vendor/github.com/docker/cli/cli-plugins/hooks/printer.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/morikuni/aec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintNextSteps(out io.Writer, messages []string) {
|
||||||
|
if len(messages) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
|
||||||
|
for _, n := range messages {
|
||||||
|
_, _ = fmt.Fprintf(out, " %s\n", n)
|
||||||
|
}
|
||||||
|
}
|
115
vendor/github.com/docker/cli/cli-plugins/hooks/template.go
generated
vendored
Normal file
115
vendor/github.com/docker/cli/cli-plugins/hooks/template.go
generated
vendored
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NextSteps = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// HookMessage represents a plugin hook response. Plugins
|
||||||
|
// declaring support for CLI hooks need to print a json
|
||||||
|
// representation of this type when their hook subcommand
|
||||||
|
// is invoked.
|
||||||
|
type HookMessage struct {
|
||||||
|
Type HookType
|
||||||
|
Template string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateReplaceSubcommandName returns a hook template string
|
||||||
|
// that will be replaced by the CLI subcommand being executed
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "you ran the subcommand: " + TemplateReplaceSubcommandName()
|
||||||
|
//
|
||||||
|
// when being executed after the command:
|
||||||
|
// `docker run --name "my-container" alpine`
|
||||||
|
// will result in the message:
|
||||||
|
// `you ran the subcommand: run`
|
||||||
|
func TemplateReplaceSubcommandName() string {
|
||||||
|
return hookTemplateCommandName
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateReplaceFlagValue returns a hook template string
|
||||||
|
// that will be replaced by the flags value.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "you ran a container named: " + TemplateReplaceFlagValue("name")
|
||||||
|
//
|
||||||
|
// when being executed after the command:
|
||||||
|
// `docker run --name "my-container" alpine`
|
||||||
|
// will result in the message:
|
||||||
|
// `you ran a container named: my-container`
|
||||||
|
func TemplateReplaceFlagValue(flag string) string {
|
||||||
|
return fmt.Sprintf(hookTemplateFlagValue, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateReplaceArg takes an index i and returns a hook
|
||||||
|
// template string that the CLI will replace the template with
|
||||||
|
// the ith argument, after processing the passed flags.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "run this image with `docker run " + TemplateReplaceArg(0) + "`"
|
||||||
|
//
|
||||||
|
// when being executed after the command:
|
||||||
|
// `docker pull alpine`
|
||||||
|
// will result in the message:
|
||||||
|
// "Run this image with `docker run alpine`"
|
||||||
|
func TemplateReplaceArg(i int) string {
|
||||||
|
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseTemplate(hookTemplate string, cmd *cobra.Command) (string, error) {
|
||||||
|
tmpl := template.New("").Funcs(commandFunctions)
|
||||||
|
tmpl, err := tmpl.Parse(hookTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
err = tmpl.Execute(&b, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrHookTemplateParse = errors.New("failed to parse hook template")
|
||||||
|
|
||||||
|
const (
|
||||||
|
hookTemplateCommandName = "{{.Name}}"
|
||||||
|
hookTemplateFlagValue = `{{flag . "%s"}}`
|
||||||
|
hookTemplateArg = "{{arg . %s}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandFunctions = template.FuncMap{
|
||||||
|
"flag": getFlagValue,
|
||||||
|
"arg": getArgValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFlagValue(cmd *cobra.Command, flag string) (string, error) {
|
||||||
|
cmdFlag := cmd.Flag(flag)
|
||||||
|
if cmdFlag == nil {
|
||||||
|
return "", ErrHookTemplateParse
|
||||||
|
}
|
||||||
|
return cmdFlag.Value.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArgValue(cmd *cobra.Command, i int) (string, error) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
if flags == nil {
|
||||||
|
return "", ErrHookTemplateParse
|
||||||
|
}
|
||||||
|
return flags.Arg(i), nil
|
||||||
|
}
|
3
vendor/github.com/docker/cli/cli-plugins/manager/error.go
generated
vendored
3
vendor/github.com/docker/cli/cli-plugins/manager/error.go
generated
vendored
@ -41,6 +41,9 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
|
|||||||
// wrapAsPluginError wraps an error in a pluginError with an
|
// wrapAsPluginError wraps an error in a pluginError with an
|
||||||
// additional message, analogous to errors.Wrapf.
|
// additional message, analogous to errors.Wrapf.
|
||||||
func wrapAsPluginError(err error, msg string) error {
|
func wrapAsPluginError(err error, msg string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return &pluginError{cause: errors.Wrap(err, msg)}
|
return &pluginError{cause: errors.Wrap(err, msg)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
127
vendor/github.com/docker/cli/cli-plugins/manager/hooks.go
generated
vendored
Normal file
127
vendor/github.com/docker/cli/cli-plugins/manager/hooks.go
generated
vendored
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli-plugins/hooks"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HookPluginData is the type representing the information
|
||||||
|
// that plugins declaring support for hooks get passed when
|
||||||
|
// being invoked following a CLI command execution.
|
||||||
|
type HookPluginData struct {
|
||||||
|
RootCmd string
|
||||||
|
Flags map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunPluginHooks calls the hook subcommand for all present
|
||||||
|
// CLI plugins that declare support for hooks in their metadata
|
||||||
|
// and parses/prints their responses.
|
||||||
|
func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, plugin string, args []string) error {
|
||||||
|
subCmdName := subCommand.Name()
|
||||||
|
if plugin != "" {
|
||||||
|
subCmdName = plugin
|
||||||
|
}
|
||||||
|
var flags map[string]string
|
||||||
|
if plugin == "" {
|
||||||
|
flags = getCommandFlags(subCommand)
|
||||||
|
} else {
|
||||||
|
flags = getNaiveFlags(args)
|
||||||
|
}
|
||||||
|
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, subCmdName, flags)
|
||||||
|
|
||||||
|
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, hookCmdName string, flags map[string]string) []string {
|
||||||
|
pluginsCfg := dockerCli.ConfigFile().Plugins
|
||||||
|
if pluginsCfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||||
|
for pluginName, cfg := range pluginsCfg {
|
||||||
|
if !registersHook(cfg, hookCmdName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hookReturn, err := p.RunHook(hookCmdName, flags)
|
||||||
|
if err != nil {
|
||||||
|
// skip misbehaving plugins, but don't halt execution
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var hookMessageData hooks.HookMessage
|
||||||
|
err = json.Unmarshal(hookReturn, &hookMessageData)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently the only hook type
|
||||||
|
if hookMessageData.Type != hooks.NextSteps {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextSteps = append(nextSteps, processedHook)
|
||||||
|
}
|
||||||
|
return nextSteps
|
||||||
|
}
|
||||||
|
|
||||||
|
func registersHook(pluginCfg map[string]string, subCmdName string) bool {
|
||||||
|
hookCmdStr, ok := pluginCfg["hooks"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
commands := strings.Split(hookCmdStr, ",")
|
||||||
|
for _, hookCmd := range commands {
|
||||||
|
if hookCmd == subCmdName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandFlags(cmd *cobra.Command) map[string]string {
|
||||||
|
flags := make(map[string]string)
|
||||||
|
cmd.Flags().Visit(func(f *pflag.Flag) {
|
||||||
|
var fValue string
|
||||||
|
if f.Value.Type() == "bool" {
|
||||||
|
fValue = f.Value.String()
|
||||||
|
}
|
||||||
|
flags[f.Name] = fValue
|
||||||
|
})
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNaiveFlags string-matches argv and parses them into a map.
|
||||||
|
// This is used when calling hooks after a plugin command, since
|
||||||
|
// in this case we can't rely on the cobra command tree to parse
|
||||||
|
// flags in this case. In this case, no values are ever passed,
|
||||||
|
// since we don't have enough information to process them.
|
||||||
|
func getNaiveFlags(args []string) map[string]string {
|
||||||
|
flags := make(map[string]string)
|
||||||
|
for _, arg := range args {
|
||||||
|
if strings.HasPrefix(arg, "--") {
|
||||||
|
flags[arg[2:]] = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
flags[arg[1:]] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flags
|
||||||
|
}
|
5
vendor/github.com/docker/cli/cli-plugins/manager/metadata.go
generated
vendored
5
vendor/github.com/docker/cli/cli-plugins/manager/metadata.go
generated
vendored
@ -8,6 +8,11 @@ const (
|
|||||||
// which must be supported by every plugin and returns the
|
// which must be supported by every plugin and returns the
|
||||||
// plugin metadata.
|
// plugin metadata.
|
||||||
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
||||||
|
|
||||||
|
// HookSubcommandName is the name of the plugin subcommand
|
||||||
|
// which must be implemented by plugins declaring support
|
||||||
|
// for hooks in their metadata.
|
||||||
|
HookSubcommandName = "docker-cli-plugin-hooks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metadata provided by the plugin.
|
// Metadata provided by the plugin.
|
||||||
|
20
vendor/github.com/docker/cli/cli-plugins/manager/plugin.go
generated
vendored
20
vendor/github.com/docker/cli/cli-plugins/manager/plugin.go
generated
vendored
@ -2,6 +2,7 @@ package manager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@ -100,3 +101,22 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
|||||||
}
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunHook executes the plugin's hooks command
|
||||||
|
// and returns its unprocessed output.
|
||||||
|
func (p *Plugin) RunHook(cmdName string, flags map[string]string) ([]byte, error) {
|
||||||
|
hDataBytes, err := json.Marshal(HookPluginData{
|
||||||
|
RootCmd: cmdName,
|
||||||
|
Flags: flags,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
||||||
|
}
|
||||||
|
|
||||||
|
hookCmdOutput, err := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes)).Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
|
||||||
|
}
|
||||||
|
|
||||||
|
return hookCmdOutput, nil
|
||||||
|
}
|
||||||
|
120
vendor/github.com/docker/cli/cli-plugins/socket/socket.go
generated
vendored
120
vendor/github.com/docker/cli/cli-plugins/socket/socket.go
generated
vendored
@ -7,24 +7,114 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EnvKey represents the well-known environment variable used to pass the plugin being
|
// EnvKey represents the well-known environment variable used to pass the
|
||||||
// executed the socket name it should listen on to coordinate with the host CLI.
|
// plugin being executed the socket name it should listen on to coordinate with
|
||||||
|
// the host CLI.
|
||||||
const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
|
const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
|
||||||
|
|
||||||
// SetupConn sets up a Unix socket listener, establishes a goroutine to handle connections
|
// NewPluginServer creates a plugin server that listens on a new Unix domain
|
||||||
// and update the conn pointer, and returns the listener for the socket (which the caller
|
// socket. h is called for each new connection to the socket in a goroutine.
|
||||||
// is responsible for closing when it's no longer needed).
|
func NewPluginServer(h func(net.Conn)) (*PluginServer, error) {
|
||||||
func SetupConn(conn **net.UnixConn) (*net.UnixListener, error) {
|
// Listen on a Unix socket, with the address being platform-dependent.
|
||||||
listener, err := listen("docker_cli_" + randomID())
|
// When a non-abstract address is used, Go will unlink(2) the socket
|
||||||
|
// for us once the listener is closed, as documented in
|
||||||
|
// [net.UnixListener.SetUnlinkOnClose].
|
||||||
|
l, err := net.ListenUnix("unix", &net.UnixAddr{
|
||||||
|
Name: socketName("docker_cli_" + randomID()),
|
||||||
|
Net: "unix",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accept(listener, conn)
|
if h == nil {
|
||||||
|
h = func(net.Conn) {}
|
||||||
|
}
|
||||||
|
|
||||||
return listener, nil
|
pl := &PluginServer{
|
||||||
|
l: l,
|
||||||
|
h: h,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer pl.Close()
|
||||||
|
for {
|
||||||
|
err := pl.accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return pl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginServer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
conns []net.Conn
|
||||||
|
l *net.UnixListener
|
||||||
|
h func(net.Conn)
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *PluginServer) accept() error {
|
||||||
|
conn, err := pl.l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pl.mu.Lock()
|
||||||
|
defer pl.mu.Unlock()
|
||||||
|
|
||||||
|
if pl.closed {
|
||||||
|
// Handle potential race between Close and accept.
|
||||||
|
conn.Close()
|
||||||
|
return errors.New("plugin server is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pl.conns = append(pl.conns, conn)
|
||||||
|
|
||||||
|
go pl.h(conn)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr returns the [net.Addr] of the underlying [net.Listener].
|
||||||
|
func (pl *PluginServer) Addr() net.Addr {
|
||||||
|
return pl.l.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close ensures that the server is no longer accepting new connections and
|
||||||
|
// closes all existing connections. Existing connections will receive [io.EOF].
|
||||||
|
//
|
||||||
|
// The error value is that of the underlying [net.Listner.Close] call.
|
||||||
|
func (pl *PluginServer) Close() error {
|
||||||
|
// Close connections first to ensure the connections get io.EOF instead
|
||||||
|
// of a connection reset.
|
||||||
|
pl.closeAllConns()
|
||||||
|
|
||||||
|
// Try to ensure that any active connections have a chance to receive
|
||||||
|
// io.EOF.
|
||||||
|
runtime.Gosched()
|
||||||
|
|
||||||
|
return pl.l.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *PluginServer) closeAllConns() {
|
||||||
|
pl.mu.Lock()
|
||||||
|
defer pl.mu.Unlock()
|
||||||
|
|
||||||
|
// Prevent new connections from being accepted.
|
||||||
|
pl.closed = true
|
||||||
|
|
||||||
|
for _, conn := range pl.conns {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
pl.conns = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomID() string {
|
func randomID() string {
|
||||||
@ -35,18 +125,6 @@ func randomID() string {
|
|||||||
return hex.EncodeToString(b)
|
return hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func accept(listener *net.UnixListener, conn **net.UnixConn) {
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// ignore error here, if we failed to accept a connection,
|
|
||||||
// conn is nil and we fallback to previous behavior
|
|
||||||
*conn, _ = listener.AcceptUnix()
|
|
||||||
// perform any platform-specific actions on accept (e.g. unlink non-abstract sockets)
|
|
||||||
onAccept(*conn, listener)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectAndWait connects to the socket passed via well-known env var,
|
// ConnectAndWait connects to the socket passed via well-known env var,
|
||||||
// if present, and attempts to read from it until it receives an EOF, at which
|
// if present, and attempts to read from it until it receives an EOF, at which
|
||||||
// point cb is called.
|
// point cb is called.
|
||||||
|
9
vendor/github.com/docker/cli/cli-plugins/socket/socket_abstract.go
generated
vendored
Normal file
9
vendor/github.com/docker/cli/cli-plugins/socket/socket_abstract.go
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
//go:build windows || linux
|
||||||
|
|
||||||
|
package socket
|
||||||
|
|
||||||
|
func socketName(basename string) string {
|
||||||
|
// Address of an abstract socket -- this socket can be opened by name,
|
||||||
|
// but is not present in the filesystem.
|
||||||
|
return "@" + basename
|
||||||
|
}
|
19
vendor/github.com/docker/cli/cli-plugins/socket/socket_darwin.go
generated
vendored
19
vendor/github.com/docker/cli/cli-plugins/socket/socket_darwin.go
generated
vendored
@ -1,19 +0,0 @@
|
|||||||
package socket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func listen(socketname string) (*net.UnixListener, error) {
|
|
||||||
return net.ListenUnix("unix", &net.UnixAddr{
|
|
||||||
Name: filepath.Join(os.TempDir(), socketname),
|
|
||||||
Net: "unix",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
|
|
||||||
syscall.Unlink(listener.Addr().String())
|
|
||||||
}
|
|
14
vendor/github.com/docker/cli/cli-plugins/socket/socket_noabstract.go
generated
vendored
Normal file
14
vendor/github.com/docker/cli/cli-plugins/socket/socket_noabstract.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//go:build !windows && !linux
|
||||||
|
|
||||||
|
package socket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func socketName(basename string) string {
|
||||||
|
// Because abstract sockets are unavailable, use a socket path in the
|
||||||
|
// system temporary directory.
|
||||||
|
return filepath.Join(os.TempDir(), basename)
|
||||||
|
}
|
20
vendor/github.com/docker/cli/cli-plugins/socket/socket_nodarwin.go
generated
vendored
20
vendor/github.com/docker/cli/cli-plugins/socket/socket_nodarwin.go
generated
vendored
@ -1,20 +0,0 @@
|
|||||||
//go:build !darwin && !openbsd
|
|
||||||
|
|
||||||
package socket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func listen(socketname string) (*net.UnixListener, error) {
|
|
||||||
return net.ListenUnix("unix", &net.UnixAddr{
|
|
||||||
Name: "@" + socketname,
|
|
||||||
Net: "unix",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
|
|
||||||
// do nothing
|
|
||||||
// while on darwin and OpenBSD we would unlink here;
|
|
||||||
// on non-darwin the socket is abstract and not present on the filesystem
|
|
||||||
}
|
|
19
vendor/github.com/docker/cli/cli-plugins/socket/socket_openbsd.go
generated
vendored
19
vendor/github.com/docker/cli/cli-plugins/socket/socket_openbsd.go
generated
vendored
@ -1,19 +0,0 @@
|
|||||||
package socket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func listen(socketname string) (*net.UnixListener, error) {
|
|
||||||
return net.ListenUnix("unix", &net.UnixAddr{
|
|
||||||
Name: filepath.Join(os.TempDir(), socketname),
|
|
||||||
Net: "unix",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
|
|
||||||
syscall.Unlink(listener.Addr().String())
|
|
||||||
}
|
|
32
vendor/github.com/docker/cli/cli/command/cli.go
generated
vendored
32
vendor/github.com/docker/cli/cli/command/cli.go
generated
vendored
@ -65,6 +65,7 @@ type Cli interface {
|
|||||||
ContextStore() store.Store
|
ContextStore() store.Store
|
||||||
CurrentContext() string
|
CurrentContext() string
|
||||||
DockerEndpoint() docker.Endpoint
|
DockerEndpoint() docker.Endpoint
|
||||||
|
TelemetryClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerCli is an instance the docker command line client.
|
// DockerCli is an instance the docker command line client.
|
||||||
@ -85,6 +86,7 @@ type DockerCli struct {
|
|||||||
dockerEndpoint docker.Endpoint
|
dockerEndpoint docker.Endpoint
|
||||||
contextStoreConfig store.Config
|
contextStoreConfig store.Config
|
||||||
initTimeout time.Duration
|
initTimeout time.Duration
|
||||||
|
res telemetryResource
|
||||||
|
|
||||||
// baseCtx is the base context used for internal operations. In the future
|
// baseCtx is the base context used for internal operations. In the future
|
||||||
// this may be replaced by explicitly passing a context to functions that
|
// this may be replaced by explicitly passing a context to functions that
|
||||||
@ -187,6 +189,36 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
|||||||
return cli.ServerInfo().OSType != "windows", nil
|
return cli.ServerInfo().OSType != "windows", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HooksEnabled returns whether plugin hooks are enabled.
|
||||||
|
func (cli *DockerCli) HooksEnabled() bool {
|
||||||
|
// legacy support DOCKER_CLI_HINTS env var
|
||||||
|
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
|
||||||
|
enabled, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
// use DOCKER_CLI_HOOKS env var value if set and not empty
|
||||||
|
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
|
||||||
|
enabled, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
featuresMap := cli.ConfigFile().Features
|
||||||
|
if v, ok := featuresMap["hooks"]; ok {
|
||||||
|
enabled, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
// default to false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// ManifestStore returns a store for local manifests
|
// ManifestStore returns a store for local manifests
|
||||||
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||||
// TODO: support override default location from config file
|
// TODO: support override default location from config file
|
||||||
|
202
vendor/github.com/docker/cli/cli/command/telemetry.go
generated
vendored
Normal file
202
vendor/github.com/docker/cli/cli/command/telemetry.go
generated
vendored
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/uuid"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
const exportTimeout = 50 * time.Millisecond
|
||||||
|
|
||||||
|
// TracerProvider is an extension of the trace.TracerProvider interface for CLI programs.
|
||||||
|
type TracerProvider interface {
|
||||||
|
trace.TracerProvider
|
||||||
|
ForceFlush(ctx context.Context) error
|
||||||
|
Shutdown(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeterProvider is an extension of the metric.MeterProvider interface for CLI programs.
|
||||||
|
type MeterProvider interface {
|
||||||
|
metric.MeterProvider
|
||||||
|
ForceFlush(ctx context.Context) error
|
||||||
|
Shutdown(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TelemetryClient provides the methods for using OTEL tracing or metrics.
|
||||||
|
type TelemetryClient interface {
|
||||||
|
// Resource returns the OTEL Resource configured with this TelemetryClient.
|
||||||
|
// This resource may be created lazily, but the resource should be the same
|
||||||
|
// each time this function is invoked.
|
||||||
|
Resource() *resource.Resource
|
||||||
|
|
||||||
|
// TracerProvider returns a TracerProvider. This TracerProvider will be configured
|
||||||
|
// with the default tracing components for a CLI program along with any options given
|
||||||
|
// for the SDK.
|
||||||
|
TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider
|
||||||
|
|
||||||
|
// MeterProvider returns a MeterProvider. This MeterProvider will be configured
|
||||||
|
// with the default metric components for a CLI program along with any options given
|
||||||
|
// for the SDK.
|
||||||
|
MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *DockerCli) Resource() *resource.Resource {
|
||||||
|
return cli.res.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *DockerCli) TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider {
|
||||||
|
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...)
|
||||||
|
return sdktrace.NewTracerProvider(allOpts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *DockerCli) MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider {
|
||||||
|
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...)
|
||||||
|
return sdkmetric.NewMeterProvider(allOpts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithResourceOptions configures additional options for the default resource. The default
|
||||||
|
// resource will continue to include its default options.
|
||||||
|
func WithResourceOptions(opts ...resource.Option) CLIOption {
|
||||||
|
return func(cli *DockerCli) error {
|
||||||
|
cli.res.AppendOptions(opts...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithResource overwrites the default resource and prevents its creation.
|
||||||
|
func WithResource(res *resource.Resource) CLIOption {
|
||||||
|
return func(cli *DockerCli) error {
|
||||||
|
cli.res.Set(res)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type telemetryResource struct {
|
||||||
|
res *resource.Resource
|
||||||
|
opts []resource.Option
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *telemetryResource) Set(res *resource.Resource) {
|
||||||
|
r.res = res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *telemetryResource) Get() *resource.Resource {
|
||||||
|
r.once.Do(r.init)
|
||||||
|
return r.res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *telemetryResource) init() {
|
||||||
|
if r.res != nil {
|
||||||
|
r.opts = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := append(r.defaultOptions(), r.opts...)
|
||||||
|
res, err := resource.New(context.Background(), opts...)
|
||||||
|
if err != nil {
|
||||||
|
otel.Handle(err)
|
||||||
|
}
|
||||||
|
r.res = res
|
||||||
|
|
||||||
|
// Clear the resource options since they'll never be used again and to allow
|
||||||
|
// the garbage collector to retrieve that memory.
|
||||||
|
r.opts = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *telemetryResource) defaultOptions() []resource.Option {
|
||||||
|
return []resource.Option{
|
||||||
|
resource.WithDetectors(serviceNameDetector{}),
|
||||||
|
resource.WithAttributes(
|
||||||
|
// Use a unique instance id so OTEL knows that each invocation
|
||||||
|
// of the CLI is its own instance. Without this, downstream
|
||||||
|
// OTEL processors may think the same process is restarting
|
||||||
|
// continuously.
|
||||||
|
semconv.ServiceInstanceID(uuid.Generate().String()),
|
||||||
|
),
|
||||||
|
resource.WithFromEnv(),
|
||||||
|
resource.WithTelemetrySDK(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *telemetryResource) AppendOptions(opts ...resource.Option) {
|
||||||
|
if r.res != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.opts = append(r.opts, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceNameDetector struct{}
|
||||||
|
|
||||||
|
func (serviceNameDetector) Detect(ctx context.Context) (*resource.Resource, error) {
|
||||||
|
return resource.StringDetector(
|
||||||
|
semconv.SchemaURL,
|
||||||
|
semconv.ServiceNameKey,
|
||||||
|
func() (string, error) {
|
||||||
|
return filepath.Base(os.Args[0]), nil
|
||||||
|
},
|
||||||
|
).Detect(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cliReader is an implementation of Reader that will automatically
|
||||||
|
// report to a designated Exporter when Shutdown is called.
|
||||||
|
type cliReader struct {
|
||||||
|
sdkmetric.Reader
|
||||||
|
exporter sdkmetric.Exporter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCLIReader(exp sdkmetric.Exporter) sdkmetric.Reader {
|
||||||
|
reader := sdkmetric.NewManualReader(
|
||||||
|
sdkmetric.WithTemporalitySelector(deltaTemporality),
|
||||||
|
)
|
||||||
|
return &cliReader{
|
||||||
|
Reader: reader,
|
||||||
|
exporter: exp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// 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
|
||||||
|
// quickly.
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, exportTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return r.exporter.Export(ctx, &rm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deltaTemporality sets the Temporality of every instrument to delta.
|
||||||
|
//
|
||||||
|
// This isn't really needed since we create a unique resource on each invocation,
|
||||||
|
// but it can help with cardinality concerns for downstream processors since they can
|
||||||
|
// perform aggregation for a time interval and then discard the data once that time
|
||||||
|
// period has passed. Cumulative temporality would imply to the downstream processor
|
||||||
|
// that they might receive a successive point and they may unnecessarily keep state
|
||||||
|
// they really shouldn't.
|
||||||
|
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
|
||||||
|
return metricdata.DeltaTemporality
|
||||||
|
}
|
142
vendor/github.com/docker/cli/cli/command/telemetry_docker.go
generated
vendored
Normal file
142
vendor/github.com/docker/cli/cli/command/telemetry_docker.go
generated
vendored
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// FIXME(jsternberg): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||||
|
//go:build go1.19
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
otelContextFieldName string = "otel"
|
||||||
|
otelExporterOTLPEndpoint string = "OTEL_EXPORTER_OTLP_ENDPOINT"
|
||||||
|
debugEnvVarPrefix string = "DOCKER_CLI_"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dockerExporterOTLPEndpoint retrieves the OTLP endpoint used for the docker reporter
|
||||||
|
// from the current context.
|
||||||
|
func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) {
|
||||||
|
meta, err := cli.ContextStore().GetMetadata(cli.CurrentContext())
|
||||||
|
if err != nil {
|
||||||
|
otel.Handle(err)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
var otelCfg any
|
||||||
|
switch m := meta.Metadata.(type) {
|
||||||
|
case DockerContext:
|
||||||
|
otelCfg = m.AdditionalFields[otelContextFieldName]
|
||||||
|
case map[string]any:
|
||||||
|
otelCfg = m[otelContextFieldName]
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
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
|
||||||
|
// (ignore otel defaults for this override when the key exists but is empty)
|
||||||
|
if override := os.Getenv(debugEnvVarPrefix + otelExporterOTLPEndpoint); override != "" {
|
||||||
|
endpoint = override
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the endpoint. The docker config expects the endpoint to be
|
||||||
|
// in the form of a URL to match the environment variable, but this
|
||||||
|
// option doesn't correspond directly to WithEndpoint.
|
||||||
|
//
|
||||||
|
// We pretend we're the same as the environment reader.
|
||||||
|
u, err := url.Parse(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
otel.Handle(errors.Errorf("docker otel endpoint is invalid: %s", err))
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "unix":
|
||||||
|
// Unix sockets are a bit weird. OTEL seems to imply they
|
||||||
|
// can be used as an environment variable and are handled properly,
|
||||||
|
// but they don't seem to be as the behavior of the environment variable
|
||||||
|
// is to strip the scheme from the endpoint, but the underlying implementation
|
||||||
|
// needs the scheme to use the correct resolver.
|
||||||
|
//
|
||||||
|
// We'll just handle this in a special way and add the unix:// back to the endpoint.
|
||||||
|
endpoint = fmt.Sprintf("unix://%s", path.Join(u.Host, u.Path))
|
||||||
|
case "https":
|
||||||
|
secure = true
|
||||||
|
fallthrough
|
||||||
|
case "http":
|
||||||
|
endpoint = path.Join(u.Host, u.Path)
|
||||||
|
}
|
||||||
|
return endpoint, secure
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerSpanExporter(ctx context.Context, cli Cli) []sdktrace.TracerProviderOption {
|
||||||
|
endpoint, secure := dockerExporterOTLPEndpoint(cli)
|
||||||
|
if endpoint == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []otlptracegrpc.Option{
|
||||||
|
otlptracegrpc.WithEndpoint(endpoint),
|
||||||
|
}
|
||||||
|
if !secure {
|
||||||
|
opts = append(opts, otlptracegrpc.WithInsecure())
|
||||||
|
}
|
||||||
|
|
||||||
|
exp, err := otlptracegrpc.New(ctx, opts...)
|
||||||
|
if err != nil {
|
||||||
|
otel.Handle(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []sdktrace.TracerProviderOption{sdktrace.WithBatcher(exp, sdktrace.WithExportTimeout(exportTimeout))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option {
|
||||||
|
endpoint, secure := dockerExporterOTLPEndpoint(cli)
|
||||||
|
if endpoint == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []otlpmetricgrpc.Option{
|
||||||
|
otlpmetricgrpc.WithEndpoint(endpoint),
|
||||||
|
}
|
||||||
|
if !secure {
|
||||||
|
opts = append(opts, otlpmetricgrpc.WithInsecure())
|
||||||
|
}
|
||||||
|
|
||||||
|
exp, err := otlpmetricgrpc.New(ctx, opts...)
|
||||||
|
if err != nil {
|
||||||
|
otel.Handle(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []sdkmetric.Option{sdkmetric.WithReader(newCLIReader(exp))}
|
||||||
|
}
|
158
vendor/github.com/docker/cli/cli/command/telemetry_utils.go
generated
vendored
Normal file
158
vendor/github.com/docker/cli/cli/command/telemetry_utils.go
generated
vendored
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/version"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseMetricAttributes returns an attribute.Set containing attributes to attach to metrics/traces
|
||||||
|
func BaseMetricAttributes(cmd *cobra.Command) attribute.Set {
|
||||||
|
attrList := []attribute.KeyValue{
|
||||||
|
attribute.String("command.name", getCommandName(cmd)),
|
||||||
|
}
|
||||||
|
return attribute.NewSet(attrList...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel.
|
||||||
|
//
|
||||||
|
// Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution.
|
||||||
|
//
|
||||||
|
// can also be used for spans!
|
||||||
|
func InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) {
|
||||||
|
meter := getDefaultMeter(mp)
|
||||||
|
// If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default
|
||||||
|
ogPersistentPreRunE := cmd.PersistentPreRunE
|
||||||
|
if ogPersistentPreRunE == nil {
|
||||||
|
ogPersistentPreRun := cmd.PersistentPreRun
|
||||||
|
//nolint:unparam // necessary because error will always be nil here
|
||||||
|
ogPersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
ogPersistentPreRun(cmd, args)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmd.PersistentPreRun = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap RunE in PersistentPreRunE so that this operation gets executed on all children commands
|
||||||
|
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
// If RunE is nil, make it execute Run and return nil by default
|
||||||
|
ogRunE := cmd.RunE
|
||||||
|
if ogRunE == nil {
|
||||||
|
ogRun := cmd.Run
|
||||||
|
//nolint:unparam // necessary because error will always be nil here
|
||||||
|
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 {
|
||||||
|
// start the timer as the first step of every cobra command
|
||||||
|
stopCobraCmdTimer := startCobraCommandTimer(cmd, meter)
|
||||||
|
cmdErr := ogRunE(cmd, args)
|
||||||
|
stopCobraCmdTimer(cmdErr)
|
||||||
|
return cmdErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return ogPersistentPreRunE(cmd, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter) func(err error) {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
baseAttrs := BaseMetricAttributes(cmd)
|
||||||
|
durationCounter, _ := meter.Float64Counter(
|
||||||
|
"command.time",
|
||||||
|
metric.WithDescription("Measures the duration of the cobra command"),
|
||||||
|
metric.WithUnit("ms"),
|
||||||
|
)
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
return func(err error) {
|
||||||
|
duration := float64(time.Since(start)) / float64(time.Millisecond)
|
||||||
|
cmdStatusAttrs := attributesFromError(err)
|
||||||
|
durationCounter.Add(ctx, duration,
|
||||||
|
metric.WithAttributeSet(baseAttrs),
|
||||||
|
metric.WithAttributeSet(attribute.NewSet(cmdStatusAttrs...)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func attributesFromError(err error) []attribute.KeyValue {
|
||||||
|
attrs := []attribute.KeyValue{}
|
||||||
|
exitCode := 0
|
||||||
|
if err != nil {
|
||||||
|
exitCode = 1
|
||||||
|
if stderr, ok := err.(statusError); ok {
|
||||||
|
// StatusError should only be used for errors, and all errors should
|
||||||
|
// have a non-zero exit status, so only set this here if this value isn't 0
|
||||||
|
if stderr.StatusCode != 0 {
|
||||||
|
exitCode = stderr.StatusCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attrs = append(attrs, attribute.String("command.error.type", otelErrorType(err)))
|
||||||
|
}
|
||||||
|
attrs = append(attrs, attribute.String("command.status.code", strconv.Itoa(exitCode)))
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// otelErrorType returns an attribute for the error type based on the error category.
|
||||||
|
func otelErrorType(err error) string {
|
||||||
|
name := "generic"
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
name = "canceled"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// statusError reports an unsuccessful exit by a command.
|
||||||
|
type statusError struct {
|
||||||
|
Status string
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e statusError) Error() string {
|
||||||
|
return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCommandName gets the cobra command name in the format
|
||||||
|
// `... parentCommandName commandName` by traversing it's parent commands recursively.
|
||||||
|
// until the root command is reached.
|
||||||
|
//
|
||||||
|
// Note: The root command's name is excluded. If cmd is the root cmd, return ""
|
||||||
|
func getCommandName(cmd *cobra.Command) string {
|
||||||
|
fullCmdName := getFullCommandName(cmd)
|
||||||
|
i := strings.Index(fullCmdName, " ")
|
||||||
|
if i == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fullCmdName[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFullCommandName gets the full cobra command name in the format
|
||||||
|
// `... parentCommandName commandName` by traversing it's parent commands recursively
|
||||||
|
// until the root command is reached.
|
||||||
|
func getFullCommandName(cmd *cobra.Command) string {
|
||||||
|
if cmd.HasParent() {
|
||||||
|
return fmt.Sprintf("%s %s", getFullCommandName(cmd.Parent()), cmd.Name())
|
||||||
|
}
|
||||||
|
return cmd.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultMeter gets the default metric.Meter for the application
|
||||||
|
// using the given metric.MeterProvider
|
||||||
|
func getDefaultMeter(mp metric.MeterProvider) metric.Meter {
|
||||||
|
return mp.Meter(
|
||||||
|
"github.com/docker/cli",
|
||||||
|
metric.WithInstrumentationVersion(version.Version),
|
||||||
|
)
|
||||||
|
}
|
1
vendor/github.com/docker/cli/cli/config/configfile/file.go
generated
vendored
1
vendor/github.com/docker/cli/cli/config/configfile/file.go
generated
vendored
@ -41,6 +41,7 @@ type ConfigFile struct {
|
|||||||
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
|
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
|
||||||
Plugins map[string]map[string]string `json:"plugins,omitempty"`
|
Plugins map[string]map[string]string `json:"plugins,omitempty"`
|
||||||
Aliases map[string]string `json:"aliases,omitempty"`
|
Aliases map[string]string `json:"aliases,omitempty"`
|
||||||
|
Features map[string]string `json:"features,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyConfig contains proxy configuration settings
|
// ProxyConfig contains proxy configuration settings
|
||||||
|
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
@ -215,9 +215,10 @@ 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.0+incompatible
|
# github.com/docker/cli v26.0.1-0.20240401150816-155dc5e4e406+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/manager
|
github.com/docker/cli/cli-plugins/manager
|
||||||
github.com/docker/cli/cli-plugins/plugin
|
github.com/docker/cli/cli-plugins/plugin
|
||||||
github.com/docker/cli/cli-plugins/socket
|
github.com/docker/cli/cli-plugins/socket
|
||||||
|
Loading…
x
Reference in New Issue
Block a user