metrics.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /*
  2. Copyright 2020 Docker, Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package metrics
  14. import (
  15. "strings"
  16. flag "github.com/spf13/pflag"
  17. )
  18. var managementCommands = []string{
  19. "app",
  20. "assemble",
  21. "builder",
  22. "buildx",
  23. "ecs",
  24. "ecs compose",
  25. "cluster",
  26. "compose",
  27. "config",
  28. "container",
  29. "context",
  30. // We add "context create" as a management command to be able to catch
  31. // calls to "context create aci"
  32. "context create",
  33. "help",
  34. "image",
  35. // Adding "login" as a management command so that the system can catch
  36. // commands like `docker login azure`
  37. "login",
  38. "manifest",
  39. "network",
  40. "node",
  41. "plugin",
  42. "registry",
  43. "secret",
  44. "service",
  45. "stack",
  46. "swarm",
  47. "system",
  48. "template",
  49. "trust",
  50. "volume",
  51. }
  52. // managementSubCommands holds a list of allowed subcommands of a management
  53. // command. For example we want to send an event for "docker login azure" but
  54. // we don't wat to send the name of the registry when the user does a
  55. // "docker login my-registry", we only want to send "login"
  56. var managementSubCommands = map[string][]string{
  57. "login": {
  58. "azure",
  59. },
  60. "context create": {
  61. "aci",
  62. },
  63. }
  64. const (
  65. scanCommand = "scan"
  66. )
  67. // Track sends the tracking analytics to Docker Desktop
  68. func Track(context string, args []string, flags *flag.FlagSet) {
  69. wasIn := make(chan bool)
  70. // Fire and forget, we don't want to slow down the user waiting for DD
  71. // metrics endpoint to respond. We could lose some events but that's ok.
  72. go func() {
  73. defer func() {
  74. _ = recover()
  75. }()
  76. wasIn <- true
  77. command := getCommand(args, flags)
  78. if command != "" {
  79. c := NewClient()
  80. c.Send(Command{
  81. Command: command,
  82. Context: context,
  83. })
  84. }
  85. }()
  86. <-wasIn
  87. }
  88. func getCommand(args []string, flags *flag.FlagSet) string {
  89. command := ""
  90. strippedArgs := stripFlags(args, flags)
  91. if len(strippedArgs) != 0 {
  92. command = strippedArgs[0]
  93. if command == scanCommand {
  94. return getScanCommand(args)
  95. }
  96. for {
  97. if contains(managementCommands, command) {
  98. if sub := getSubCommand(command, strippedArgs[1:]); sub != "" {
  99. command += " " + sub
  100. strippedArgs = strippedArgs[1:]
  101. continue
  102. }
  103. }
  104. break
  105. }
  106. }
  107. return command
  108. }
  109. func getScanCommand(args []string) string {
  110. command := args[0]
  111. if contains(args, "--auth") {
  112. return command + " auth"
  113. }
  114. if contains(args, "--version") {
  115. return command + " version"
  116. }
  117. return command
  118. }
  119. func getSubCommand(command string, args []string) string {
  120. if len(args) == 0 {
  121. return ""
  122. }
  123. if val, ok := managementSubCommands[command]; ok {
  124. if contains(val, args[0]) {
  125. return args[0]
  126. }
  127. return ""
  128. }
  129. if isArg(args[0]) {
  130. return args[0]
  131. }
  132. return ""
  133. }
  134. func contains(array []string, needle string) bool {
  135. for _, val := range array {
  136. if val == needle {
  137. return true
  138. }
  139. }
  140. return false
  141. }
  142. func stripFlags(args []string, flags *flag.FlagSet) []string {
  143. commands := []string{}
  144. for len(args) > 0 {
  145. s := args[0]
  146. args = args[1:]
  147. if s == "--" {
  148. return commands
  149. }
  150. if flagArg(s, flags) {
  151. if len(args) <= 1 {
  152. return commands
  153. }
  154. args = args[1:]
  155. }
  156. if isArg(s) {
  157. commands = append(commands, s)
  158. }
  159. }
  160. return commands
  161. }
  162. func flagArg(s string, flags *flag.FlagSet) bool {
  163. return strings.HasPrefix(s, "--") && !strings.Contains(s, "=") && !hasNoOptDefVal(s[2:], flags) ||
  164. strings.HasPrefix(s, "-") && !strings.Contains(s, "=") && len(s) == 2 && !shortHasNoOptDefVal(s[1:], flags)
  165. }
  166. func isArg(s string) bool {
  167. return s != "" && !strings.HasPrefix(s, "-")
  168. }
  169. func hasNoOptDefVal(name string, fs *flag.FlagSet) bool {
  170. flag := fs.Lookup(name)
  171. if flag == nil {
  172. return false
  173. }
  174. return flag.NoOptDefVal != ""
  175. }
  176. func shortHasNoOptDefVal(name string, fs *flag.FlagSet) bool {
  177. flag := fs.ShorthandLookup(name[:1])
  178. if flag == nil {
  179. return false
  180. }
  181. return flag.NoOptDefVal != ""
  182. }