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

Commit

Permalink
add build metrics
Browse files Browse the repository at this point in the history
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
  • Loading branch information
crazy-max committed Nov 16, 2021
1 parent a4ae60a commit af82c55
Show file tree
Hide file tree
Showing 10 changed files with 367 additions and 8 deletions.
9 changes: 5 additions & 4 deletions cli/metrics/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 []byte `json:"metadata,omitempty"`
}

// CLISource is sent for cli metrics
Expand Down
2 changes: 2 additions & 0 deletions cli/metrics/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 27 additions & 4 deletions cli/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package metrics

import (
"encoding/json"
"os"
"strings"

Expand All @@ -30,12 +31,19 @@ func Track(context string, args []string, status string) {
}
command := GetCommand(args)
if command != "" {
var metadata []byte
if m := GetMetadata(command, args); (Metadata{}) != m {
if b, err := json.Marshal(m); err == nil {
metadata = b
}
}
c := NewClient()
c.Send(Command{
Command: command,
Context: context,
Source: CLISource,
Status: status,
Command: command,
Context: context,
Source: CLISource,
Status: status,
Metadata: metadata,
})
}
}
Expand Down Expand Up @@ -89,3 +97,18 @@ func GetCommand(args []string) string {
}
return result
}

type Metadata struct {
Build BuildMetadata `json:"build,omitempty"`
}

// GetMetadata returns the metadata linked to the invoked command
func GetMetadata(command string, args []string) Metadata {
var m Metadata
if command == "build" || command == "buildx" {
if bm := getBuildMetadata(command, args); bm != nil {
m.Build = *bm
}
}
return m
}
196 changes: 196 additions & 0 deletions cli/metrics/metrics_build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
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 metrics

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"
"github.com/docker/docker/api/types"
dockerclient "github.com/docker/docker/client"
"github.com/spf13/pflag"
)

type BuildMetadata struct {
Cli string `json:"cli"`
Builder string `json:"builder"`
}

// getBuildMetadata returns build metadata for this command
func getBuildMetadata(command string, args []string) *BuildMetadata {
var bm *BuildMetadata
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()
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/<builder>`
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
}
87 changes: 87 additions & 0 deletions cli/metrics/metrics_build_test.go
Original file line number Diff line number Diff line change
@@ -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 metrics

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

0 comments on commit af82c55

Please sign in to comment.