1
0
mirror of https://github.com/docker/compose-cli.git synced 2026-10-23 18:09:06 +03:00

Merge pull request #2151 from crazy-max/build-metrics-futureproof

build metrics compatibility for next 22.06
This commit is contained in:
Nick Sieger
2022-07-27 12:01:04 -05:00
committed by GitHub
10 changed files with 164 additions and 57 deletions

View File

@@ -61,6 +61,7 @@ import (
)
var (
metricsClient metrics.Client
contextAgnosticCommands = map[string]struct{}{
"context": {},
"login": {},
@@ -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())
}
@@ -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) {
@@ -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 {
@@ -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)
}
@@ -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)
@@ -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)
}
}

View File

@@ -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"`
@@ -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) {
@@ -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() {

View File

@@ -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)
}
@@ -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)
}

View File

@@ -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))
})
}
}

View File

@@ -1,29 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metadata
// Get returns the JSON metadata linked to the invoked command
func Get(cliSource string, args []string) string {
if len(args) == 0 {
return cliSource
}
switch args[0] {
case "build", "buildx":
cliSource = getBuildMetadata(cliSource, args[0], args[1:])
}
return cliSource
}

View File

@@ -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")

View File

@@ -18,6 +18,7 @@ package mobycli
import (
"context"
"debug/buildinfo"
"fmt"
"os"
"os/exec"
@@ -25,15 +26,16 @@ import (
"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}
@@ -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)
}
@@ -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)
}
@@ -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 {

View File

@@ -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)
}

4
go.mod
View File

@@ -30,8 +30,10 @@ require (
github.com/golang/mock v1.5.0
github.com/golang/protobuf v1.5.2
github.com/google/go-cmp v0.5.5
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/go-version v1.4.0
github.com/iancoleman/strcase v0.1.2
github.com/joho/godotenv v1.3.0
github.com/labstack/echo v3.3.10+incompatible
@@ -115,7 +117,6 @@ require (
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/google/btree v1.0.0 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/googleapis/gnostic v0.4.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
@@ -124,7 +125,6 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-version v1.3.0 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/huandu/xstrings v1.3.1 // indirect
github.com/imdario/mergo v0.3.12 // indirect

4
go.sum
View File

@@ -787,8 +787,8 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4=
github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=