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 коммит произвёл GitHub
Родитель 4dc3e196a4 668b262605
Коммит 6135c5edf1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 164 добавлений и 57 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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 {

Просмотреть файл

@ -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
Просмотреть файл

@ -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
Просмотреть файл

@ -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=