Hardcoded list of commands and simplified metrics gathering.

Signed-off-by: Guillaume Tardif <guillaume.tardif@docker.com>
This commit is contained in:
Guillaume Tardif 2020-10-07 23:29:55 +02:00
parent 005c6dcfe7
commit 0d3f7186c5
7 changed files with 333 additions and 224 deletions

View File

@ -198,37 +198,37 @@ func main() {
if err = root.ExecuteContext(ctx); err != nil { if err = root.ExecuteContext(ctx); err != nil {
// if user canceled request, simply exit without any error message // if user canceled request, simply exit without any error message
if errdefs.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) { if errdefs.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.CanceledStatus) metrics.Track(ctype, os.Args[1:], metrics.CanceledStatus)
os.Exit(130) os.Exit(130)
} }
if ctype == store.AwsContextType { if ctype == store.AwsContextType {
exit(root, currentContext, errors.Errorf(`%q context type has been renamed. Recreate the context by running: exit(currentContext, errors.Errorf(`%q context type has been renamed. Recreate the context by running:
$ docker context create %s <name>`, cc.Type(), store.EcsContextType), ctype) $ docker context create %s <name>`, cc.Type(), store.EcsContextType), ctype)
} }
// Context should always be handled by new CLI // Context should always be handled by new CLI
requiredCmd, _, _ := root.Find(os.Args[1:]) requiredCmd, _, _ := root.Find(os.Args[1:])
if requiredCmd != nil && isContextAgnosticCommand(requiredCmd) { if requiredCmd != nil && isContextAgnosticCommand(requiredCmd) {
exit(root, currentContext, err, ctype) exit(currentContext, err, ctype)
} }
mobycli.ExecIfDefaultCtxType(ctx, root) mobycli.ExecIfDefaultCtxType(ctx, root)
checkIfUnknownCommandExistInDefaultContext(err, currentContext, root, ctype) checkIfUnknownCommandExistInDefaultContext(err, currentContext, ctype)
exit(root, currentContext, err, ctype) exit(currentContext, err, ctype)
} }
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.SuccessStatus) metrics.Track(ctype, os.Args[1:], metrics.SuccessStatus)
} }
func exit(root *cobra.Command, ctx string, err error, ctype string) { func exit(ctx string, err error, ctype string) {
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.FailureStatus) metrics.Track(ctype, os.Args[1:], metrics.FailureStatus)
if errors.Is(err, errdefs.ErrLoginRequired) { if errors.Is(err, errdefs.ErrLoginRequired) {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(errdefs.ExitCodeLoginRequired) os.Exit(errdefs.ExitCodeLoginRequired)
} }
if errors.Is(err, errdefs.ErrNotImplemented) { if errors.Is(err, errdefs.ErrNotImplemented) {
name := metrics.GetCommand(os.Args[1:], root.PersistentFlags()) name := metrics.GetCommand(os.Args[1:])
fmt.Fprintf(os.Stderr, "Command %q not available in current context (%s)\n", name, ctx) fmt.Fprintf(os.Stderr, "Command %q not available in current context (%s)\n", name, ctx)
os.Exit(1) os.Exit(1)
@ -242,14 +242,14 @@ func fatal(err error) {
os.Exit(1) os.Exit(1)
} }
func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string, root *cobra.Command, contextType string) { func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string, contextType string) {
submatch := unknownCommandRegexp.FindSubmatch([]byte(err.Error())) submatch := unknownCommandRegexp.FindSubmatch([]byte(err.Error()))
if len(submatch) == 2 { if len(submatch) == 2 {
dockerCommand := string(submatch[1]) dockerCommand := string(submatch[1])
if mobycli.IsDefaultContextCommand(dockerCommand) { 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) 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:], root.PersistentFlags(), metrics.FailureStatus) metrics.Track(contextType, os.Args[1:], metrics.FailureStatus)
os.Exit(1) os.Exit(1)
} }
} }

View File

@ -86,7 +86,7 @@ func Exec(root *cobra.Command) {
err := cmd.Run() err := cmd.Run()
childExit <- true childExit <- true
if err != nil { if err != nil {
metrics.Track(store.DefaultContextType, os.Args[1:], root.PersistentFlags(), metrics.FailureStatus) metrics.Track(store.DefaultContextType, os.Args[1:], metrics.FailureStatus)
if exiterr, ok := err.(*exec.ExitError); ok { if exiterr, ok := err.(*exec.ExitError); ok {
os.Exit(exiterr.ExitCode()) os.Exit(exiterr.ExitCode())
@ -94,7 +94,7 @@ func Exec(root *cobra.Command) {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
} }
metrics.Track(store.DefaultContextType, os.Args[1:], root.PersistentFlags(), metrics.SuccessStatus) metrics.Track(store.DefaultContextType, os.Args[1:], metrics.SuccessStatus)
os.Exit(0) os.Exit(0)
} }

148
metrics/commands.go Normal file
View File

@ -0,0 +1,148 @@
/*
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
var commandFlags = []string{
//added to catch scan details
"--version", "--login",
"--help", "-h"}
// Generated with generatecommands/main.go
var managementCommands = []string{
"ecs",
"assemble",
"registry",
"template",
"cluster",
"app",
"builder",
"buildx",
"imagetools",
"checkpoint",
"config",
"container",
"context",
"image",
"manifest",
"network",
"node",
"plugin",
"scan",
"secret",
"service",
"stack",
"swarm",
"system",
"trust",
"key",
"signer",
"volume",
"login",
"create",
"compose",
}
var commands = []string{
"bundle",
"completion",
"init",
"inspect",
"install",
"list",
"merge",
"pull",
"push",
"render",
"split",
"status",
"uninstall",
"upgrade",
"validate",
"version",
"build",
"prune",
"create",
"bake",
"du",
"ls",
"rm",
"stop",
"use",
"attach",
"commit",
"cp",
"diff",
"exec",
"export",
"kill",
"logs",
"pause",
"port",
"rename",
"restart",
"run",
"start",
"stats",
"top",
"unpause",
"update",
"wait",
"show",
"history",
"import",
"load",
"save",
"tag",
"annotate",
"connect",
"disconnect",
"demote",
"promote",
"ps",
"disable",
"enable",
"set",
"rollback",
"scale",
"deploy",
"services",
"ca",
"join",
"join-token",
"leave",
"unlock",
"unlock-key",
"df",
"events",
"info",
"generate",
"add",
"remove",
"revoke",
"sign",
"images",
"login",
"logout",
"rmi",
"search",
"azure",
"aci",
"ecs",
"convert",
"down",
"up",
}

View File

@ -0,0 +1,89 @@
/*
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 main
import (
"fmt"
"os/exec"
"strings"
"github.com/docker/compose-cli/utils"
)
var managementCommands = []string{"ecs", "assemble", "registry", "template", "cluster"}
var commands = []string{}
func main() {
getCommands()
getCommands("login")
getCommands("context", "create")
getCommands("compose")
fmt.Printf(`
var managementCommands = []string{
"%s",
}
var commands = []string{
"%s",
}
`, strings.Join(managementCommands, "\", \n\t\""), strings.Join(commands, "\", \n\t\""))
}
func getCommands(execCommands ...string) {
if len(execCommands) > 0 {
managementCommands = append(managementCommands, execCommands[len(execCommands)-1])
}
withHelp := append(execCommands, "--help")
cmd := exec.Command("docker", withHelp...)
output, err := cmd.Output()
if err != nil {
return
}
text := string(output)
lines := strings.Split(text, "\n")
mgtCommandsStarted := false
commandsStarted := false
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "Management Commands:") {
mgtCommandsStarted = true
continue
}
if strings.HasPrefix(trimmedLine, "Commands:") || strings.HasPrefix(trimmedLine, "Available Commands:") {
mgtCommandsStarted = false
commandsStarted = true
continue
}
if trimmedLine == "" {
mgtCommandsStarted = false
commandsStarted = false
continue
}
tokens := strings.Split(trimmedLine, " ")
command := strings.Replace(tokens[0], "*", "", 1)
if mgtCommandsStarted {
getCommands(append(execCommands, command)...)
}
if commandsStarted {
if !utils.StringContains(commands, command) {
commands = append(commands, command)
}
}
}
}

View File

@ -17,68 +17,12 @@
package metrics package metrics
import ( import (
"strings"
flag "github.com/spf13/pflag"
"github.com/docker/compose-cli/utils" "github.com/docker/compose-cli/utils"
) )
var managementCommands = []string{
"app",
"assemble",
"builder",
"buildx",
"ecs",
"ecs compose",
"cluster",
"compose",
"config",
"container",
"context",
// We add "context create" as a management command to be able to catch
// calls to "context create aci"
"context create",
"help",
"image",
// Adding "login" as a management command so that the system can catch
// commands like `docker login azure`
"login",
"manifest",
"network",
"node",
"plugin",
"registry",
"secret",
"service",
"stack",
"swarm",
"system",
"template",
"trust",
"volume",
}
// managementSubCommands holds a list of allowed subcommands of a management
// command. For example we want to send an event for "docker login azure" but
// we don't wat to send the name of the registry when the user does a
// "docker login my-registry", we only want to send "login"
var managementSubCommands = map[string][]string{
"login": {
"azure",
},
"context create": {
"aci",
},
}
const (
scanCommand = "scan"
)
// Track sends the tracking analytics to Docker Desktop // Track sends the tracking analytics to Docker Desktop
func Track(context string, args []string, flags *flag.FlagSet, status string) { func Track(context string, args []string, status string) {
command := GetCommand(args, flags) command := GetCommand(args)
if command != "" { if command != "" {
c := NewClient() c := NewClient()
c.Send(Command{ c.Send(Command{
@ -90,115 +34,36 @@ func Track(context string, args []string, flags *flag.FlagSet, status string) {
} }
} }
func isCommand(word string) bool {
return utils.StringContains(commands, word) || isManagementCommand(word)
}
func isManagementCommand(word string) bool {
return utils.StringContains(managementCommands, word)
}
func isCommandFlag(word string) bool {
return utils.StringContains(commandFlags, word)
}
// GetCommand get the invoked command // GetCommand get the invoked command
func GetCommand(args []string, flags *flag.FlagSet) string { func GetCommand(args []string) string {
command := "" result := ""
strippedArgs := stripFlags(args, flags) onlyFlags := false
for _, arg := range args {
if len(strippedArgs) != 0 { if arg == "--" {
command = strippedArgs[0]
if command == scanCommand {
return getScanCommand(args)
}
for {
if utils.StringContains(managementCommands, command) {
if sub := getSubCommand(command, strippedArgs[1:]); sub != "" {
command += " " + sub
strippedArgs = strippedArgs[1:]
continue
}
}
break break
} }
} if isCommandFlag(arg) || (!onlyFlags && isCommand(arg)) {
if result == "" {
return command result = arg
} } else {
result = result + " " + arg
func getScanCommand(args []string) string { }
command := args[0] if !isManagementCommand(arg) {
onlyFlags = true
if utils.StringContains(args, "--auth") {
return command + " auth"
}
if utils.StringContains(args, "--version") {
return command + " version"
}
return command
}
func getSubCommand(command string, args []string) string {
if len(args) == 0 {
return ""
}
if val, ok := managementSubCommands[command]; ok {
if utils.StringContains(val, args[0]) {
return args[0]
}
return ""
}
if isArg(args[0]) {
return args[0]
}
return ""
}
func stripFlags(args []string, flags *flag.FlagSet) []string {
commands := []string{}
for len(args) > 0 {
s := args[0]
args = args[1:]
if s == "--" {
return commands
}
if flagArg(s, flags) {
if len(args) <= 1 {
return commands
} }
args = args[1:]
}
if isArg(s) {
commands = append(commands, s)
} }
} }
return result
return commands
}
func flagArg(s string, flags *flag.FlagSet) bool {
return strings.HasPrefix(s, "--") && !strings.Contains(s, "=") && !hasNoOptDefVal(s[2:], flags) ||
strings.HasPrefix(s, "-") && !strings.Contains(s, "=") && len(s) == 2 && !shortHasNoOptDefVal(s[1:], flags)
}
func isArg(s string) bool {
return s != "" && !strings.HasPrefix(s, "-")
}
func hasNoOptDefVal(name string, fs *flag.FlagSet) bool {
flag := fs.Lookup(name)
if flag == nil {
return false
}
return flag.NoOptDefVal != ""
}
func shortHasNoOptDefVal(name string, fs *flag.FlagSet) bool {
flag := fs.ShorthandLookup(name[:1])
if flag == nil {
return false
}
return flag.NoOptDefVal != ""
} }

View File

@ -19,15 +19,10 @@ package metrics
import ( import (
"testing" "testing"
"github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
func TestFlag(t *testing.T) { func TestGetCommand(t *testing.T) {
root := &cobra.Command{}
root.PersistentFlags().BoolP("debug", "D", false, "debug")
root.PersistentFlags().String("str", "str", "str")
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
@ -58,26 +53,11 @@ func TestFlag(t *testing.T) {
args: []string{"--debug", "--str", "str-value"}, args: []string{"--debug", "--str", "str-value"},
expected: "", expected: "",
}, },
{
name: "with unknown short flag",
args: []string{"-f", "run"},
expected: "",
},
{
name: "with unknown long flag",
args: []string{"--unknown-flag", "run"},
expected: "",
},
{ {
name: "management command", name: "management command",
args: []string{"image", "ls"}, args: []string{"image", "ls"},
expected: "image ls", expected: "image ls",
}, },
{
name: "management command with flag",
args: []string{"image", "--test", "ls"},
expected: "image",
},
{ {
name: "management subcommand with flag", name: "management subcommand with flag",
args: []string{"image", "ls", "-q"}, args: []string{"image", "ls", "-q"},
@ -93,14 +73,9 @@ func TestFlag(t *testing.T) {
args: []string{"login", "-u", "test", "azure"}, args: []string{"login", "-u", "test", "azure"},
expected: "login azure", expected: "login azure",
}, },
{
name: "azure login with azure user",
args: []string{"login", "-u", "azure"},
expected: "login",
},
{ {
name: "login to a registry", name: "login to a registry",
args: []string{"login", "registry"}, args: []string{"login", "myregistry"},
expected: "login", expected: "login",
}, },
{ {
@ -119,9 +94,9 @@ func TestFlag(t *testing.T) {
expected: "create", expected: "create",
}, },
{ {
name: "create a container named aci", name: "start a container named aci",
args: []string{"create", "aci"}, args: []string{"start", "aci"},
expected: "create", expected: "start",
}, },
{ {
name: "create a container named test-container", name: "create a container named test-container",
@ -152,15 +127,49 @@ func TestFlag(t *testing.T) {
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
result := GetCommand(testCase.args, root.PersistentFlags()) result := GetCommand(testCase.args)
assert.Equal(t, testCase.expected, result)
})
}
}
func TestFlags(t *testing.T) {
testCases := []struct {
name string
args []string
expected string
}{
{
name: "help",
args: []string{"--help"},
expected: "--help",
},
{
name: "help on run",
args: []string{"run", "--help"},
expected: "run --help",
},
{
name: "-h on run",
args: []string{"run", "-h"},
expected: "run -h",
},
{
name: "help on compose up",
args: []string{"compose", "up", "--help"},
expected: "compose up --help",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := GetCommand(testCase.args)
assert.Equal(t, testCase.expected, result) assert.Equal(t, testCase.expected, result)
}) })
} }
} }
func TestEcs(t *testing.T) { func TestEcs(t *testing.T) {
root := &cobra.Command{}
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
@ -217,23 +226,21 @@ func TestEcs(t *testing.T) {
expected: "ecs compose logs", expected: "ecs compose logs",
}, },
{ {
name: "setup", name: "ecs",
args: []string{"ecs", "setup"}, args: []string{"ecs", "anything"},
expected: "ecs setup", expected: "ecs",
}, },
} }
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
result := GetCommand(testCase.args, root.PersistentFlags()) result := GetCommand(testCase.args)
assert.Equal(t, testCase.expected, result) assert.Equal(t, testCase.expected, result)
}) })
} }
} }
func TestScan(t *testing.T) { func TestScan(t *testing.T) {
root := &cobra.Command{}
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
@ -246,34 +253,34 @@ func TestScan(t *testing.T) {
}, },
{ {
name: "scan image with long flags", name: "scan image with long flags",
args: []string{"scan", "--file", "file", "image"}, args: []string{"scan", "--file", "file", "myimage"},
expected: "scan", expected: "scan",
}, },
{ {
name: "scan image with short flags", name: "scan image with short flags",
args: []string{"scan", "-f", "file", "image"}, args: []string{"scan", "-f", "file", "myimage"},
expected: "scan", expected: "scan",
}, },
{ {
name: "scan with long flag", name: "scan with long flag",
args: []string{"scan", "--dependency-tree", "image"}, args: []string{"scan", "--dependency-tree", "myimage"},
expected: "scan", expected: "scan",
}, },
{ {
name: "auth", name: "auth",
args: []string{"scan", "--auth"}, args: []string{"scan", "--login"},
expected: "scan auth", expected: "scan --login",
}, },
{ {
name: "version", name: "version",
args: []string{"scan", "--version"}, args: []string{"scan", "--version"},
expected: "scan version", expected: "scan --version",
}, },
} }
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
result := GetCommand(testCase.args, root.PersistentFlags()) result := GetCommand(testCase.args)
assert.Equal(t, testCase.expected, result) assert.Equal(t, testCase.expected, result)
}) })
} }

View File

@ -185,7 +185,7 @@ func TestContextMetrics(t *testing.T) {
c.RunDockerCmd("ps") c.RunDockerCmd("ps")
c.RunDockerCmd("context", "use", "test-example") c.RunDockerCmd("context", "use", "test-example")
c.RunDockerCmd("ps") c.RunDockerCmd("ps")
c.RunDockerOrExitError("error") c.RunDockerOrExitError("stop", "unknown")
c.RunDockerCmd("context", "use", "default") c.RunDockerCmd("context", "use", "default")
c.RunDockerCmd("--context", "test-example", "ps") c.RunDockerCmd("--context", "test-example", "ps")
@ -195,7 +195,7 @@ func TestContextMetrics(t *testing.T) {
assert.Equal(t, `{"command":"ps","context":"moby","source":"cli","status":"success"}`, usage[1]) assert.Equal(t, `{"command":"ps","context":"moby","source":"cli","status":"success"}`, usage[1])
assert.Equal(t, `{"command":"context use","context":"moby","source":"cli","status":"success"}`, usage[2]) assert.Equal(t, `{"command":"context use","context":"moby","source":"cli","status":"success"}`, usage[2])
assert.Equal(t, `{"command":"ps","context":"example","source":"cli","status":"success"}`, usage[3]) assert.Equal(t, `{"command":"ps","context":"example","source":"cli","status":"success"}`, usage[3])
assert.Equal(t, `{"command":"error","context":"example","source":"cli","status":"failure"}`, usage[4]) assert.Equal(t, `{"command":"stop","context":"example","source":"cli","status":"failure"}`, usage[4])
assert.Equal(t, `{"command":"context use","context":"example","source":"cli","status":"success"}`, usage[5]) assert.Equal(t, `{"command":"context use","context":"example","source":"cli","status":"success"}`, usage[5])
assert.Equal(t, `{"command":"ps","context":"example","source":"cli","status":"success"}`, usage[6]) assert.Equal(t, `{"command":"ps","context":"example","source":"cli","status":"success"}`, usage[6])
}) })