diff --git a/cli/metrics/client.go b/cli/metrics/client.go index 570a135ebd..4340f5da7d 100644 --- a/cli/metrics/client.go +++ b/cli/metrics/client.go @@ -32,10 +32,11 @@ type client struct { // Command is a command type Command struct { - Command string `json:"command"` - Context string `json:"context"` - Source string `json:"source"` - Status string `json:"status"` + Command string `json:"command"` + Context string `json:"context"` + Source string `json:"source"` + Status string `json:"status"` + Metadata string `json:"metadata,omitempty"` } // CLISource is sent for cli metrics diff --git a/cli/metrics/commands.go b/cli/metrics/commands.go index ee44cf7e60..0fef9f5489 100644 --- a/cli/metrics/commands.go +++ b/cli/metrics/commands.go @@ -19,6 +19,8 @@ package metrics var commandFlags = []string{ //added to catch scan details "--version", "--login", + // added for build + "--builder", "--platforms", } // Generated with generatecommands/main.go diff --git a/cli/metrics/metadata/build.go b/cli/metrics/metadata/build.go new file mode 100644 index 0000000000..8e6bc49f59 --- /dev/null +++ b/cli/metrics/metadata/build.go @@ -0,0 +1,192 @@ +/* + 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 + +import ( + "context" + "encoding/json" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "strconv" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/configfile" + metadatatypes "github.com/docker/compose-cli/cli/metrics/metadata/types" + "github.com/docker/docker/api/types" + dockerclient "github.com/docker/docker/client" + "github.com/spf13/pflag" +) + +// getBuildMetadata returns build metadata for this command +func getBuildMetadata(command string, args []string) metadatatypes.Build { + var bm metadatatypes.Build + dockercfg := config.LoadDefaultConfigFile(io.Discard) + if alias, ok := dockercfg.Aliases["builder"]; ok { + command = alias + } + if command == "build" { + // TODO(@crazy-max): include cli version (e.g., docker;20.10.10) + bm.Cli = "docker" + bm.Builder = "buildkit" + if enabled, _ := isBuildKitEnabled(); !enabled { + bm.Builder = "legacy" + } + } else if command == "buildx" { + // TODO(@crazy-max): include buildx version (e.g., buildx;0.6.3) + bm.Cli = "buildx" + bm.Builder = buildxDriver(dockercfg, args) + } + return bm +} + +// isBuildKitEnabled returns whether buildkit is enabled either through a +// daemon setting or otherwise the client-side DOCKER_BUILDKIT environment +// variable +func isBuildKitEnabled() (bool, error) { + if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); len(buildkitEnv) > 0 { + return strconv.ParseBool(buildkitEnv) + } + apiClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) + if err != nil { + return false, err + } + defer apiClient.Close() //nolint:errcheck + ping, err := apiClient.Ping(context.Background()) + if err != nil { + return false, err + } + return ping.BuilderVersion == types.BuilderBuildKit, nil +} + +// buildxConfigDir will look for correct configuration store path; +// if `$BUILDX_CONFIG` is set - use it, otherwise use parent directory +// of Docker config file (i.e. `${DOCKER_CONFIG}/buildx`) +func buildxConfigDir(dockercfg *configfile.ConfigFile) string { + if buildxConfig := os.Getenv("BUILDX_CONFIG"); buildxConfig != "" { + return buildxConfig + } + return filepath.Join(filepath.Dir(dockercfg.Filename), "buildx") +} + +// buildxDriver returns the build driver being used for the build command +func buildxDriver(dockercfg *configfile.ConfigFile, buildArgs []string) string { + driver := "error" + configDir := buildxConfigDir(dockercfg) + if _, err := os.Stat(configDir); err != nil { + return driver + } + builder := buildxBuilder(buildArgs) + if len(builder) == 0 { + // if builder not defined in command, seek current in buildx store + // `${DOCKER_CONFIG}/buildx/current` + fileCurrent := path.Join(configDir, "current") + if _, err := os.Stat(fileCurrent); err != nil { + return driver + } + // content looks like + // { + // "Key": "unix:///var/run/docker.sock", + // "Name": "builder", + // "Global": false + // } + rawCurrent, err := ioutil.ReadFile(fileCurrent) + if err != nil { + return driver + } + // unmarshal and returns `Name` + var obj map[string]interface{} + if err = json.Unmarshal(rawCurrent, &obj); err != nil { + return driver + } + if n, ok := obj["Name"]; ok { + builder = n.(string) + // `Name` will be empty if `default` builder is used + // { + // "Key": "unix:///var/run/docker.sock", + // "Name": "", + // "Global": false + // } + if len(builder) == 0 { + builder = "default" + } + } else { + return driver + } + } + + // if default builder return docker + if builder == "default" { + return "docker" + } + + // read builder info and retrieve the current driver + // `${DOCKER_CONFIG}/buildx/instances/` + fileBuilder := path.Join(configDir, "instances", builder) + if _, err := os.Stat(fileBuilder); err != nil { + return driver + } + // content looks like + // { + // "Name": "builder", + // "Driver": "docker-container", + // "Nodes": [ + // { + // "Name": "builder0", + // "Endpoint": "unix:///var/run/docker.sock", + // "Platforms": null, + // "Flags": null, + // "ConfigFile": "", + // "DriverOpts": null + // } + // ], + // "Dynamic": false + // } + rawBuilder, err := ioutil.ReadFile(fileBuilder) + if err != nil { + return driver + } + // unmarshal and returns `Driver` + var obj map[string]interface{} + if err = json.Unmarshal(rawBuilder, &obj); err != nil { + return driver + } + if d, ok := obj["Driver"]; ok { + driver = d.(string) + } + // TODO(@crazy-max): include buildkit version being used by this driver (e.g., docker-container;0.9.2) + return driver +} + +// buildxBuilder returns the builder being used in the build command +func buildxBuilder(buildArgs []string) string { + var builder string + fset := pflag.NewFlagSet("buildx", pflag.ContinueOnError) + fset.String("builder", "", "") + _ = fset.ParseAll(buildArgs, func(flag *pflag.Flag, value string) error { + if flag.Name == "builder" { + builder = value + } + return nil + }) + if len(builder) == 0 { + builder = os.Getenv("BUILDX_BUILDER") + } + return builder +} diff --git a/cli/metrics/metadata/build_test.go b/cli/metrics/metadata/build_test.go new file mode 100644 index 0000000000..0cd281777c --- /dev/null +++ b/cli/metrics/metadata/build_test.go @@ -0,0 +1,87 @@ +/* + 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 + +import ( + "io" + "os" + "testing" + + "github.com/docker/cli/cli/config" + "gotest.tools/v3/assert" +) + +func TestBuildxBuilder(t *testing.T) { + tts := []struct { + name string + args []string + expected string + }{ + { + name: "without builder", + args: []string{"build", "-t", "foo:bar", "."}, + expected: "", + }, + { + name: "with builder", + args: []string{"--builder", "foo", "build", "."}, + expected: "foo", + }, + } + for _, tt := range tts { + t.Run(tt.name, func(t *testing.T) { + result := buildxBuilder(tt.args) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBuildxDriver(t *testing.T) { + tts := []struct { + name string + cfg string + args []string + expected string + }{ + { + name: "no flag and default builder", + cfg: "./testdata/buildx-default", + args: []string{"build", "-t", "foo:bar", "."}, + expected: "docker", + }, + { + name: "no flag and current builder", + cfg: "./testdata/buildx-container", + args: []string{"build", "-t", "foo:bar", "."}, + expected: "docker-container", + }, + { + name: "builder flag", + cfg: "./testdata/buildx-default", + args: []string{"--builder", "graviton2", "build", "."}, + expected: "docker-container", + }, + } + + for _, tt := range tts { + t.Run(tt.name, func(t *testing.T) { + _ = os.Setenv("BUILDX_CONFIG", tt.cfg) + result := buildxDriver(config.LoadDefaultConfigFile(io.Discard), tt.args) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/cli/metrics/metadata/metadata.go b/cli/metrics/metadata/metadata.go new file mode 100644 index 0000000000..1ee1e97047 --- /dev/null +++ b/cli/metrics/metadata/metadata.go @@ -0,0 +1,37 @@ +/* + 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 + +import ( + "encoding/json" + + metadatatypes "github.com/docker/compose-cli/cli/metrics/metadata/types" +) + +// Get returns the JSON metadata linked to the invoked command +func Get(command string, args []string) string { + var m metadatatypes.Metadata + if command == "build" || command == "buildx" { + m.Build = getBuildMetadata(command, args) + } + if (metadatatypes.Metadata{}) != m { + if b, err := json.Marshal(m); err == nil { + return string(b) + } + } + return "" +} diff --git a/cli/metrics/metadata/testdata/buildx-container/current b/cli/metrics/metadata/testdata/buildx-container/current new file mode 100644 index 0000000000..1ae8f69da6 --- /dev/null +++ b/cli/metrics/metadata/testdata/buildx-container/current @@ -0,0 +1 @@ +{"Key":"unix:///var/run/docker.sock","Name":"builder","Global":false} \ No newline at end of file diff --git a/cli/metrics/metadata/testdata/buildx-container/instances/builder b/cli/metrics/metadata/testdata/buildx-container/instances/builder new file mode 100644 index 0000000000..f12c83a8e5 --- /dev/null +++ b/cli/metrics/metadata/testdata/buildx-container/instances/builder @@ -0,0 +1 @@ +{"Name":"builder","Driver":"docker-container","Nodes":[{"Name":"builder0","Endpoint":"unix:///var/run/docker.sock","Platforms":null,"Flags":["--allow-insecure-entitlement","security.insecure","--allow-insecure-entitlement","network.host"],"DriverOpts":{"env.JAEGER_TRACE":"localhost:6831","image":"moby/buildkit:latest","network":"host"},"Files":null}],"Dynamic":false} \ No newline at end of file diff --git a/cli/metrics/metadata/testdata/buildx-default/current b/cli/metrics/metadata/testdata/buildx-default/current new file mode 100644 index 0000000000..f34f695d0d --- /dev/null +++ b/cli/metrics/metadata/testdata/buildx-default/current @@ -0,0 +1 @@ +{"Key":"unix:///var/run/docker.sock","Name":"","Global":false} \ No newline at end of file diff --git a/cli/metrics/metadata/testdata/buildx-default/instances/graviton2 b/cli/metrics/metadata/testdata/buildx-default/instances/graviton2 new file mode 100644 index 0000000000..ab45e1c090 --- /dev/null +++ b/cli/metrics/metadata/testdata/buildx-default/instances/graviton2 @@ -0,0 +1 @@ +{"Name":"graviton2","Driver":"docker-container","Nodes":[{"Name":"node1","Endpoint":"ssh://ubuntu@18.246.77.142","Platforms":[{"architecture":"arm64","os":"linux"}],"Flags":null,"ConfigFile":"","DriverOpts":{}}],"Dynamic":false} \ No newline at end of file diff --git a/cli/metrics/metadata/types/types.go b/cli/metrics/metadata/types/types.go new file mode 100644 index 0000000000..1ad3301a93 --- /dev/null +++ b/cli/metrics/metadata/types/types.go @@ -0,0 +1,26 @@ +/* + 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 metadatatypes + +type Metadata struct { + Build Build `json:"build,omitempty"` +} + +type Build struct { + Cli string `json:"cli,omitempty"` + Builder string `json:"builder,omitempty"` +} diff --git a/cli/metrics/metrics.go b/cli/metrics/metrics.go index 7840892c30..426c94a858 100644 --- a/cli/metrics/metrics.go +++ b/cli/metrics/metrics.go @@ -20,6 +20,7 @@ import ( "os" "strings" + "github.com/docker/compose-cli/cli/metrics/metadata" "github.com/docker/compose/v2/pkg/utils" ) @@ -32,10 +33,11 @@ func Track(context string, args []string, status string) { if command != "" { c := NewClient() c.Send(Command{ - Command: command, - Context: context, - Source: CLISource, - Status: status, + Command: command, + Context: context, + Source: CLISource, + Status: status, + Metadata: metadata.Get(command, args), }) } } diff --git a/cli/metrics/metrics_test.go b/cli/metrics/metrics_test.go index 8711a07b85..5c4a7175cd 100644 --- a/cli/metrics/metrics_test.go +++ b/cli/metrics/metrics_test.go @@ -335,3 +335,49 @@ func TestScan(t *testing.T) { }) } } + +func TestBuild(t *testing.T) { + testCases := []struct { + name string + args []string + expected string + }{ + { + name: "build", + args: []string{"build", "."}, + expected: "build", + }, + { + name: "build with flags", + args: []string{"build", "--file", "./Dockerfile", "--tag", "myimage:latest", "."}, + expected: "build", + }, + { + name: "buildx build", + args: []string{"buildx", "build", "."}, + expected: "buildx build", + }, + { + name: "buildx build with flags", + args: []string{"buildx", "build", "--file", "./Dockerfile", "--tag", "myimage:latest", "."}, + expected: "buildx build", + }, + { + name: "buildx build with flags and builder", + args: []string{"buildx", "--builder", "foo", "build", "--file", "./Dockerfile", "--tag", "myimage:latest", "."}, + expected: "buildx --builder build", + }, + { + name: "buildx version", + args: []string{"buildx", "version"}, + expected: "buildx version", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := GetCommand(testCase.args) + assert.Equal(t, testCase.expected, result) + }) + } +} diff --git a/local/e2e/cli-only/e2e_test.go b/local/e2e/cli-only/e2e_test.go index 896387e2bb..e56fd547d0 100644 --- a/local/e2e/cli-only/e2e_test.go +++ b/local/e2e/cli-only/e2e_test.go @@ -210,6 +210,22 @@ func TestContextMetrics(t *testing.T) { `{"command":"context ls","context":"moby","source":"cli","status":"success"}`, }, usage) }) + + t.Run("metrics on build", func(t *testing.T) { + s.ResetUsage() + + _ = os.Setenv("BUILDX_CONFIG", "./testdata/buildx-config") + c.RunDockerOrExitError("build", "-t", "foo:bar", ".") + c.RunDockerOrExitError("buildx", "build", "-t", "foo:bar", ".") + c.RunDockerOrExitError("buildx", "--builder", "graviton2", "build", "-t", "foo:bar", ".") + + usage := s.GetUsage() + assert.DeepEqual(t, []string{ + `{"command":"context create","context":"moby","source":"cli","status":"success"}`, + `{"command":"ps","context":"moby","source":"cli","status":"success"}`, + `{"command":"context use","context":"moby","source":"cli","status":"success"}`, + }, usage) + }) } func TestContextDuplicateACI(t *testing.T) { diff --git a/local/e2e/cli-only/testdata/buildx-config/current b/local/e2e/cli-only/testdata/buildx-config/current new file mode 100644 index 0000000000..1ae8f69da6 --- /dev/null +++ b/local/e2e/cli-only/testdata/buildx-config/current @@ -0,0 +1 @@ +{"Key":"unix:///var/run/docker.sock","Name":"builder","Global":false} \ No newline at end of file diff --git a/local/e2e/cli-only/testdata/buildx-config/instances/builder b/local/e2e/cli-only/testdata/buildx-config/instances/builder new file mode 100644 index 0000000000..f12c83a8e5 --- /dev/null +++ b/local/e2e/cli-only/testdata/buildx-config/instances/builder @@ -0,0 +1 @@ +{"Name":"builder","Driver":"docker-container","Nodes":[{"Name":"builder0","Endpoint":"unix:///var/run/docker.sock","Platforms":null,"Flags":["--allow-insecure-entitlement","security.insecure","--allow-insecure-entitlement","network.host"],"DriverOpts":{"env.JAEGER_TRACE":"localhost:6831","image":"moby/buildkit:latest","network":"host"},"Files":null}],"Dynamic":false} \ No newline at end of file diff --git a/local/e2e/cli-only/testdata/buildx-config/instances/graviton2 b/local/e2e/cli-only/testdata/buildx-config/instances/graviton2 new file mode 100644 index 0000000000..ab45e1c090 --- /dev/null +++ b/local/e2e/cli-only/testdata/buildx-config/instances/graviton2 @@ -0,0 +1 @@ +{"Name":"graviton2","Driver":"docker-container","Nodes":[{"Name":"node1","Endpoint":"ssh://ubuntu@18.246.77.142","Platforms":[{"architecture":"arm64","os":"linux"}],"Flags":null,"ConfigFile":"","DriverOpts":{}}],"Dynamic":false} \ No newline at end of file