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

Commit

Permalink
Merge pull request #2151 from crazy-max/build-metrics-futureproof
Browse files Browse the repository at this point in the history
build metrics compatibility for next 22.06
  • Loading branch information
nicksieger authored Jul 27, 2022
2 parents 4dc3e19 + 668b262 commit 6135c5e
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 57 deletions.
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

0 comments on commit 6135c5e

Please sign in to comment.