Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import (
)

var (
metricsClient metrics.Client
contextAgnosticCommands = map[string]struct{}{
"context": {},
"login": {},
Expand All @@ -86,6 +87,12 @@ func init() {
if err := os.Setenv("PATH", appendPaths(os.Getenv("PATH"), path)); err != nil {
panic(err)
}

metricsClient = metrics.NewClient()
metricsClient.WithCliVersionFunc(func() string {
return mobycli.CliVersion()
})

// Seed random
rand.Seed(time.Now().UnixNano())
}
Expand Down Expand Up @@ -249,7 +256,7 @@ func main() {
if err = root.ExecuteContext(ctx); err != nil {
handleError(ctx, err, ctype, currentContext, cc, root)
}
metrics.Track(ctype, os.Args[1:], compose.SuccessStatus)
metricsClient.Track(ctype, os.Args[1:], compose.SuccessStatus)
}

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

func exit(ctx string, err error, ctype string) {
if exit, ok := err.(cli.StatusError); ok {
metrics.Track(ctype, os.Args[1:], compose.SuccessStatus)
metricsClient.Track(ctype, os.Args[1:], compose.SuccessStatus)
os.Exit(exit.StatusCode)
}

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

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

if mobycli.IsDefaultContextCommand(dockerCommand) {
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)
metrics.Track(contextType, os.Args[1:], compose.FailureStatus)
metricsClient.Track(contextType, os.Args[1:], compose.FailureStatus)
os.Exit(1)
}
}
Expand Down
18 changes: 17 additions & 1 deletion cli/metrics/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ import (
)

type client struct {
cliversion *cliversion
httpClient *http.Client
}

type cliversion struct {
version string
f func() string
}

// Command is a command
type Command struct {
Command string `json:"command"`
Expand All @@ -47,17 +53,23 @@ func init() {
}
}

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

// NewClient returns a new metrics client
func NewClient() Client {
return &client{
cliversion: &cliversion{},
httpClient: &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
Expand All @@ -68,6 +80,10 @@ func NewClient() Client {
}
}

func (c *client) WithCliVersionFunc(f func() string) {
c.cliversion.f = f
}

func (c *client) Send(command Command) {
result := make(chan bool, 1)
go func() {
Expand Down
46 changes: 39 additions & 7 deletions cli/metrics/metadata/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,34 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/docker/api/types"
dockerclient "github.com/docker/docker/client"
"github.com/hashicorp/go-version"
"github.com/spf13/pflag"
)

// getBuildMetadata returns build metadata for this command
func getBuildMetadata(cliSource string, command string, args []string) string {
// BuildMetadata returns build metadata for this command
func BuildMetadata(cliSource, cliVersion, command string, args []string) string {
var cli, builder string
dockercfg := config.LoadDefaultConfigFile(io.Discard)
if alias, ok := dockercfg.Aliases["builder"]; ok {
if alias != "buildx" {
return cliSource
}
command = alias
}
if command == "build" {
cli = "docker"
builder = "buildkit"
if enabled, _ := isBuildKitEnabled(); !enabled {
builder = "legacy"
buildkitEnabled, _ := isBuildKitEnabled()
if buildkitEnabled && isBuildxDefault(cliVersion) {
command = "buildx"
args = append([]string{"build"}, args...)
} else {
cli = "docker"
builder = "buildkit"
if !buildkitEnabled {
builder = "legacy"
}
}
} else if command == "buildx" {
}
if command == "buildx" {
cli = "buildx"
builder = buildxDriver(dockercfg, args)
}
Expand Down Expand Up @@ -183,3 +194,24 @@ func buildxBuilder(buildArgs []string) string {
}
return builder
}

// isBuildxDefault returns true if buildx by default is used
// through "docker build" command which is already an alias to
// "docker buildx build" in docker cli.
// more info: https://github.com/docker/cli/pull/3314
func isBuildxDefault(cliVersion string) bool {
if cliVersion == "" {
// empty means DWARF symbol table is stripped from cli binary
// which is the case with docker cli < 22.06
return false
}
verCurrent, err := version.NewVersion(cliVersion)
if err != nil {
return false
}
// 21.0.0 is an arbitrary version number because next major is not
// intended to be 21 but 22 and buildx by default will never be part
// of a 20 release version anyway.
verBuildxDefault, _ := version.NewVersion("21.0.0")
return verCurrent.GreaterThanOrEqual(verBuildxDefault)
}
29 changes: 29 additions & 0 deletions cli/metrics/metadata/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,32 @@ func TestBuildxDriver(t *testing.T) {
})
}
}

func TestIsBuildxDefault(t *testing.T) {
tts := []struct {
cliVersion string
expected bool
}{
{
cliVersion: "",
expected: false,
},
{
cliVersion: "20.10.15",
expected: false,
},
{
cliVersion: "20.10.2-575-g22edabb584.m",
expected: false,
},
{
cliVersion: "22.05.0",
expected: true,
},
}
for _, tt := range tts {
t.Run(tt.cliVersion, func(t *testing.T) {
assert.Equal(t, tt.expected, isBuildxDefault(tt.cliVersion))
})
}
}
29 changes: 0 additions & 29 deletions cli/metrics/metadata/metadata.go

This file was deleted.

17 changes: 13 additions & 4 deletions cli/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,32 @@ import (
"github.com/docker/compose/v2/pkg/utils"
)

// Track sends the tracking analytics to Docker Desktop
func Track(context string, args []string, status string) {
func (c *client) Track(context string, args []string, status string) {
if isInvokedAsCliBackend() {
return
}
command := GetCommand(args)
if command != "" {
c := NewClient()
c.Send(Command{
Command: command,
Context: context,
Source: metadata.Get(CLISource, args),
Source: c.getMetadata(CLISource, args),
Status: status,
})
}
}

func (c *client) getMetadata(cliSource string, args []string) string {
if len(args) == 0 {
return cliSource
}
switch args[0] {
case "build", "buildx":
cliSource = metadata.BuildMetadata(cliSource, c.cliversion.f(), args[0], args[1:])
}
return cliSource
}

func isInvokedAsCliBackend() bool {
executable := os.Args[0]
return strings.HasSuffix(executable, "-backend")
Expand Down
49 changes: 42 additions & 7 deletions cli/mobycli/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,24 @@ package mobycli

import (
"context"
"debug/buildinfo"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"runtime"

"github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v2/pkg/utils"
"github.com/spf13/cobra"
"strings"

apicontext "github.com/docker/compose-cli/api/context"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/cli/metrics"
"github.com/docker/compose-cli/cli/mobycli/resolvepath"
"github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v2/pkg/utils"
"github.com/google/shlex"
"github.com/spf13/cobra"
)

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

// Exec delegates to com.docker.cli if on moby context
func Exec(root *cobra.Command) {
metricsClient := metrics.NewClient()
metricsClient.WithCliVersionFunc(func() string {
return CliVersion()
})
childExit := make(chan bool)
err := RunDocker(childExit, os.Args[1:]...)
childExit <- true
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
exitCode := exiterr.ExitCode()
metrics.Track(store.DefaultContextType, os.Args[1:], compose.ByExitCode(exitCode).MetricsStatus)
metricsClient.Track(store.DefaultContextType, os.Args[1:], compose.ByExitCode(exitCode).MetricsStatus)
os.Exit(exitCode)
}
metrics.Track(store.DefaultContextType, os.Args[1:], compose.FailureStatus)
metricsClient.Track(store.DefaultContextType, os.Args[1:], compose.FailureStatus)
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
Expand All @@ -92,7 +98,7 @@ func Exec(root *cobra.Command) {
if command == "login" && !metrics.HasQuietFlag(commandArgs) {
displayPATSuggestMsg(commandArgs)
}
metrics.Track(store.DefaultContextType, os.Args[1:], compose.SuccessStatus)
metricsClient.Track(store.DefaultContextType, os.Args[1:], compose.SuccessStatus)

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

// CliVersion returns the docker cli version
func CliVersion() string {
info, err := buildinfo.ReadFile(ComDockerCli)
if err != nil {
return ""
}
for _, s := range info.Settings {
if s.Key != "-ldflags" {
continue
}
args, err := shlex.Split(s.Value)
if err != nil {
return ""
}
for _, a := range args {
// https://github.com/docker/cli/blob/f1615facb1ca44e4336ab20e621315fc2cfb845a/scripts/build/.variables#L77
if !strings.HasPrefix(a, "github.com/docker/cli/cli/version.Version") {
continue
}
parts := strings.Split(a, "=")
if len(parts) != 2 {
return ""
}
return parts[1]
}
}
return ""
}

// ExecSilent executes a command and do redirect output to stdOut, return output
func ExecSilent(ctx context.Context, args ...string) ([]byte, error) {
if len(args) == 0 {
Expand Down
8 changes: 8 additions & 0 deletions cli/server/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ type mockMetricsClient struct {
mock.Mock
}

func (s *mockMetricsClient) WithCliVersionFunc(f func() string) {
s.Called(f)
}

func (s *mockMetricsClient) Send(command metrics.Command) {
s.Called(command)
}

func (s *mockMetricsClient) Track(context string, args []string, status string) {
s.Called(context, args, status)
}
Loading