Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit 6135c5e

Browse files
authored
Merge pull request #2151 from crazy-max/build-metrics-futureproof
build metrics compatibility for next 22.06
2 parents 4dc3e19 + 668b262 commit 6135c5e

File tree

10 files changed

+164
-57
lines changed

10 files changed

+164
-57
lines changed

cli/main.go

+12-5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import (
6161
)
6262

6363
var (
64+
metricsClient metrics.Client
6465
contextAgnosticCommands = map[string]struct{}{
6566
"context": {},
6667
"login": {},
@@ -86,6 +87,12 @@ func init() {
8687
if err := os.Setenv("PATH", appendPaths(os.Getenv("PATH"), path)); err != nil {
8788
panic(err)
8889
}
90+
91+
metricsClient = metrics.NewClient()
92+
metricsClient.WithCliVersionFunc(func() string {
93+
return mobycli.CliVersion()
94+
})
95+
8996
// Seed random
9097
rand.Seed(time.Now().UnixNano())
9198
}
@@ -249,7 +256,7 @@ func main() {
249256
if err = root.ExecuteContext(ctx); err != nil {
250257
handleError(ctx, err, ctype, currentContext, cc, root)
251258
}
252-
metrics.Track(ctype, os.Args[1:], compose.SuccessStatus)
259+
metricsClient.Track(ctype, os.Args[1:], compose.SuccessStatus)
253260
}
254261

255262
func customizeCliForACI(command *cobra.Command, proxy *api.ServiceProxy) {
@@ -271,7 +278,7 @@ func customizeCliForACI(command *cobra.Command, proxy *api.ServiceProxy) {
271278
func handleError(ctx context.Context, err error, ctype string, currentContext string, cc *store.DockerContext, root *cobra.Command) {
272279
// if user canceled request, simply exit without any error message
273280
if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
274-
metrics.Track(ctype, os.Args[1:], compose.CanceledStatus)
281+
metricsClient.Track(ctype, os.Args[1:], compose.CanceledStatus)
275282
os.Exit(130)
276283
}
277284
if ctype == store.AwsContextType {
@@ -293,7 +300,7 @@ $ docker context create %s <name>`, cc.Type(), store.EcsContextType), ctype)
293300

294301
func exit(ctx string, err error, ctype string) {
295302
if exit, ok := err.(cli.StatusError); ok {
296-
metrics.Track(ctype, os.Args[1:], compose.SuccessStatus)
303+
metricsClient.Track(ctype, os.Args[1:], compose.SuccessStatus)
297304
os.Exit(exit.StatusCode)
298305
}
299306

@@ -308,7 +315,7 @@ func exit(ctx string, err error, ctype string) {
308315
metricsStatus = compose.CommandSyntaxFailure.MetricsStatus
309316
exitCode = compose.CommandSyntaxFailure.ExitCode
310317
}
311-
metrics.Track(ctype, os.Args[1:], metricsStatus)
318+
metricsClient.Track(ctype, os.Args[1:], metricsStatus)
312319

313320
if errors.Is(err, api.ErrLoginRequired) {
314321
fmt.Fprintln(os.Stderr, err)
@@ -343,7 +350,7 @@ func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string
343350

344351
if mobycli.IsDefaultContextCommand(dockerCommand) {
345352
fmt.Fprintf(os.Stderr, "Command %q not available in current context (%s), you can use the \"default\" context to run this command\n", dockerCommand, currentContext)
346-
metrics.Track(contextType, os.Args[1:], compose.FailureStatus)
353+
metricsClient.Track(contextType, os.Args[1:], compose.FailureStatus)
347354
os.Exit(1)
348355
}
349356
}

cli/metrics/client.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ import (
2727
)
2828

2929
type client struct {
30+
cliversion *cliversion
3031
httpClient *http.Client
3132
}
3233

34+
type cliversion struct {
35+
version string
36+
f func() string
37+
}
38+
3339
// Command is a command
3440
type Command struct {
3541
Command string `json:"command"`
@@ -47,17 +53,23 @@ func init() {
4753
}
4854
}
4955

50-
// Client sends metrics to Docker Desktopn
56+
// Client sends metrics to Docker Desktop
5157
type Client interface {
58+
// WithCliVersionFunc sets the docker cli version func
59+
// that returns the docker cli version (com.docker.cli)
60+
WithCliVersionFunc(f func() string)
5261
// Send sends the command to Docker Desktop. Note that the function doesn't
5362
// return anything, not even an error, this is because we don't really care
5463
// if the metrics were sent or not. We only fire and forget.
5564
Send(Command)
65+
// Track sends the tracking analytics to Docker Desktop
66+
Track(context string, args []string, status string)
5667
}
5768

5869
// NewClient returns a new metrics client
5970
func NewClient() Client {
6071
return &client{
72+
cliversion: &cliversion{},
6173
httpClient: &http.Client{
6274
Transport: &http.Transport{
6375
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
@@ -68,6 +80,10 @@ func NewClient() Client {
6880
}
6981
}
7082

83+
func (c *client) WithCliVersionFunc(f func() string) {
84+
c.cliversion.f = f
85+
}
86+
7187
func (c *client) Send(command Command) {
7288
result := make(chan bool, 1)
7389
go func() {

cli/metrics/metadata/build.go

+39-7
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,34 @@ import (
3131
"github.com/docker/cli/cli/config/configfile"
3232
"github.com/docker/docker/api/types"
3333
dockerclient "github.com/docker/docker/client"
34+
"github.com/hashicorp/go-version"
3435
"github.com/spf13/pflag"
3536
)
3637

37-
// getBuildMetadata returns build metadata for this command
38-
func getBuildMetadata(cliSource string, command string, args []string) string {
38+
// BuildMetadata returns build metadata for this command
39+
func BuildMetadata(cliSource, cliVersion, command string, args []string) string {
3940
var cli, builder string
4041
dockercfg := config.LoadDefaultConfigFile(io.Discard)
4142
if alias, ok := dockercfg.Aliases["builder"]; ok {
43+
if alias != "buildx" {
44+
return cliSource
45+
}
4246
command = alias
4347
}
4448
if command == "build" {
45-
cli = "docker"
46-
builder = "buildkit"
47-
if enabled, _ := isBuildKitEnabled(); !enabled {
48-
builder = "legacy"
49+
buildkitEnabled, _ := isBuildKitEnabled()
50+
if buildkitEnabled && isBuildxDefault(cliVersion) {
51+
command = "buildx"
52+
args = append([]string{"build"}, args...)
53+
} else {
54+
cli = "docker"
55+
builder = "buildkit"
56+
if !buildkitEnabled {
57+
builder = "legacy"
58+
}
4959
}
50-
} else if command == "buildx" {
60+
}
61+
if command == "buildx" {
5162
cli = "buildx"
5263
builder = buildxDriver(dockercfg, args)
5364
}
@@ -183,3 +194,24 @@ func buildxBuilder(buildArgs []string) string {
183194
}
184195
return builder
185196
}
197+
198+
// isBuildxDefault returns true if buildx by default is used
199+
// through "docker build" command which is already an alias to
200+
// "docker buildx build" in docker cli.
201+
// more info: https://github.com/docker/cli/pull/3314
202+
func isBuildxDefault(cliVersion string) bool {
203+
if cliVersion == "" {
204+
// empty means DWARF symbol table is stripped from cli binary
205+
// which is the case with docker cli < 22.06
206+
return false
207+
}
208+
verCurrent, err := version.NewVersion(cliVersion)
209+
if err != nil {
210+
return false
211+
}
212+
// 21.0.0 is an arbitrary version number because next major is not
213+
// intended to be 21 but 22 and buildx by default will never be part
214+
// of a 20 release version anyway.
215+
verBuildxDefault, _ := version.NewVersion("21.0.0")
216+
return verCurrent.GreaterThanOrEqual(verBuildxDefault)
217+
}

cli/metrics/metadata/build_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,32 @@ func TestBuildxDriver(t *testing.T) {
8585
})
8686
}
8787
}
88+
89+
func TestIsBuildxDefault(t *testing.T) {
90+
tts := []struct {
91+
cliVersion string
92+
expected bool
93+
}{
94+
{
95+
cliVersion: "",
96+
expected: false,
97+
},
98+
{
99+
cliVersion: "20.10.15",
100+
expected: false,
101+
},
102+
{
103+
cliVersion: "20.10.2-575-g22edabb584.m",
104+
expected: false,
105+
},
106+
{
107+
cliVersion: "22.05.0",
108+
expected: true,
109+
},
110+
}
111+
for _, tt := range tts {
112+
t.Run(tt.cliVersion, func(t *testing.T) {
113+
assert.Equal(t, tt.expected, isBuildxDefault(tt.cliVersion))
114+
})
115+
}
116+
}

cli/metrics/metadata/metadata.go

-29
This file was deleted.

cli/metrics/metrics.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,32 @@ import (
2424
"github.com/docker/compose/v2/pkg/utils"
2525
)
2626

27-
// Track sends the tracking analytics to Docker Desktop
28-
func Track(context string, args []string, status string) {
27+
func (c *client) Track(context string, args []string, status string) {
2928
if isInvokedAsCliBackend() {
3029
return
3130
}
3231
command := GetCommand(args)
3332
if command != "" {
34-
c := NewClient()
3533
c.Send(Command{
3634
Command: command,
3735
Context: context,
38-
Source: metadata.Get(CLISource, args),
36+
Source: c.getMetadata(CLISource, args),
3937
Status: status,
4038
})
4139
}
4240
}
4341

42+
func (c *client) getMetadata(cliSource string, args []string) string {
43+
if len(args) == 0 {
44+
return cliSource
45+
}
46+
switch args[0] {
47+
case "build", "buildx":
48+
cliSource = metadata.BuildMetadata(cliSource, c.cliversion.f(), args[0], args[1:])
49+
}
50+
return cliSource
51+
}
52+
4453
func isInvokedAsCliBackend() bool {
4554
executable := os.Args[0]
4655
return strings.HasSuffix(executable, "-backend")

cli/mobycli/exec.go

+42-7
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,24 @@ package mobycli
1818

1919
import (
2020
"context"
21+
"debug/buildinfo"
2122
"fmt"
2223
"os"
2324
"os/exec"
2425
"os/signal"
2526
"path/filepath"
2627
"regexp"
2728
"runtime"
28-
29-
"github.com/docker/compose/v2/pkg/compose"
30-
"github.com/docker/compose/v2/pkg/utils"
31-
"github.com/spf13/cobra"
29+
"strings"
3230

3331
apicontext "github.com/docker/compose-cli/api/context"
3432
"github.com/docker/compose-cli/api/context/store"
3533
"github.com/docker/compose-cli/cli/metrics"
3634
"github.com/docker/compose-cli/cli/mobycli/resolvepath"
35+
"github.com/docker/compose/v2/pkg/compose"
36+
"github.com/docker/compose/v2/pkg/utils"
37+
"github.com/google/shlex"
38+
"github.com/spf13/cobra"
3739
)
3840

3941
var delegatedContextTypes = []string{store.DefaultContextType}
@@ -71,16 +73,20 @@ func mustDelegateToMoby(ctxType string) bool {
7173

7274
// Exec delegates to com.docker.cli if on moby context
7375
func Exec(root *cobra.Command) {
76+
metricsClient := metrics.NewClient()
77+
metricsClient.WithCliVersionFunc(func() string {
78+
return CliVersion()
79+
})
7480
childExit := make(chan bool)
7581
err := RunDocker(childExit, os.Args[1:]...)
7682
childExit <- true
7783
if err != nil {
7884
if exiterr, ok := err.(*exec.ExitError); ok {
7985
exitCode := exiterr.ExitCode()
80-
metrics.Track(store.DefaultContextType, os.Args[1:], compose.ByExitCode(exitCode).MetricsStatus)
86+
metricsClient.Track(store.DefaultContextType, os.Args[1:], compose.ByExitCode(exitCode).MetricsStatus)
8187
os.Exit(exitCode)
8288
}
83-
metrics.Track(store.DefaultContextType, os.Args[1:], compose.FailureStatus)
89+
metricsClient.Track(store.DefaultContextType, os.Args[1:], compose.FailureStatus)
8490
fmt.Fprintln(os.Stderr, err)
8591
os.Exit(1)
8692
}
@@ -92,7 +98,7 @@ func Exec(root *cobra.Command) {
9298
if command == "login" && !metrics.HasQuietFlag(commandArgs) {
9399
displayPATSuggestMsg(commandArgs)
94100
}
95-
metrics.Track(store.DefaultContextType, os.Args[1:], compose.SuccessStatus)
101+
metricsClient.Track(store.DefaultContextType, os.Args[1:], compose.SuccessStatus)
96102

97103
os.Exit(0)
98104
}
@@ -174,6 +180,35 @@ func IsDefaultContextCommand(dockerCommand string) bool {
174180
return regexp.MustCompile("Usage:\\s*docker\\s*" + dockerCommand).Match(b)
175181
}
176182

183+
// CliVersion returns the docker cli version
184+
func CliVersion() string {
185+
info, err := buildinfo.ReadFile(ComDockerCli)
186+
if err != nil {
187+
return ""
188+
}
189+
for _, s := range info.Settings {
190+
if s.Key != "-ldflags" {
191+
continue
192+
}
193+
args, err := shlex.Split(s.Value)
194+
if err != nil {
195+
return ""
196+
}
197+
for _, a := range args {
198+
// https://github.com/docker/cli/blob/f1615facb1ca44e4336ab20e621315fc2cfb845a/scripts/build/.variables#L77
199+
if !strings.HasPrefix(a, "github.com/docker/cli/cli/version.Version") {
200+
continue
201+
}
202+
parts := strings.Split(a, "=")
203+
if len(parts) != 2 {
204+
return ""
205+
}
206+
return parts[1]
207+
}
208+
}
209+
return ""
210+
}
211+
177212
// ExecSilent executes a command and do redirect output to stdOut, return output
178213
func ExecSilent(ctx context.Context, args ...string) ([]byte, error) {
179214
if len(args) == 0 {

cli/server/metrics_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ type mockMetricsClient struct {
122122
mock.Mock
123123
}
124124

125+
func (s *mockMetricsClient) WithCliVersionFunc(f func() string) {
126+
s.Called(f)
127+
}
128+
125129
func (s *mockMetricsClient) Send(command metrics.Command) {
126130
s.Called(command)
127131
}
132+
133+
func (s *mockMetricsClient) Track(context string, args []string, status string) {
134+
s.Called(context, args, status)
135+
}

0 commit comments

Comments
 (0)