|
| 1 | +/* |
| 2 | + Copyright 2022 Docker Compose CLI authors |
| 3 | +
|
| 4 | + Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + you may not use this file except in compliance with the License. |
| 6 | + You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | + Unless required by applicable law or agreed to in writing, software |
| 11 | + distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + See the License for the specific language governing permissions and |
| 14 | + limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package metrics |
| 18 | + |
| 19 | +import ( |
| 20 | + "strings" |
| 21 | + "time" |
| 22 | +) |
| 23 | + |
| 24 | +// DockerCLIEvent represents an invocation of `docker` from the the CLI. |
| 25 | +type DockerCLIEvent struct { |
| 26 | + Command string `json:"command,omitempty"` |
| 27 | + Subcommand string `json:"subcommand,omitempty"` |
| 28 | + Usage bool `json:"usage,omitempty"` |
| 29 | + ExitCode int32 `json:"exit_code"` |
| 30 | + StartTime time.Time `json:"start_time"` |
| 31 | + DurationSecs float64 `json:"duration_secs,omitempty"` |
| 32 | +} |
| 33 | + |
| 34 | +// NewDockerCLIEvent inspects the command line string and returns a stripped down |
| 35 | +// version suitable for reporting. |
| 36 | +// |
| 37 | +// The parser will only use known values for command/subcommand from a hardcoded |
| 38 | +// built-in set for safety. It also does not attempt to perfectly accurately |
| 39 | +// reflect how arg parsing works in a real program, instead favoring a fairly |
| 40 | +// simple approach that's still reasonably robust. |
| 41 | +// |
| 42 | +// If the command does not map to a known Docker (or first-party plugin) |
| 43 | +// command, `nil` will be returned. Similarly, if no subcommand for the |
| 44 | +// built-in/plugin can be determined, it will be empty. |
| 45 | +func NewDockerCLIEvent(cmd CmdResult) *DockerCLIEvent { |
| 46 | + if len(cmd.Args) == 0 { |
| 47 | + return nil |
| 48 | + } |
| 49 | + |
| 50 | + cmdPath := findCommand(append([]string{"docker"}, cmd.Args...)) |
| 51 | + if cmdPath == nil { |
| 52 | + return nil |
| 53 | + } |
| 54 | + |
| 55 | + if len(cmdPath) < 2 { |
| 56 | + // ignore unknown commands; we can't infer anything from them safely |
| 57 | + // N.B. ONLY compose commands are supported by `cmdHierarchy` currently! |
| 58 | + return nil |
| 59 | + } |
| 60 | + |
| 61 | + // look for a subcommand |
| 62 | + var subcommand string |
| 63 | + if len(cmdPath) >= 3 { |
| 64 | + var subcommandParts []string |
| 65 | + for _, c := range cmdPath[2:] { |
| 66 | + subcommandParts = append(subcommandParts, c.name) |
| 67 | + } |
| 68 | + subcommand = strings.Join(subcommandParts, "-") |
| 69 | + } |
| 70 | + |
| 71 | + var usage bool |
| 72 | + for _, arg := range cmd.Args { |
| 73 | + // TODO(milas): also support `docker help build` syntax |
| 74 | + if arg == "help" { |
| 75 | + return nil |
| 76 | + } |
| 77 | + |
| 78 | + if arg == "--help" || arg == "-h" { |
| 79 | + usage = true |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + event := &DockerCLIEvent{ |
| 84 | + Command: cmdPath[1].name, |
| 85 | + Subcommand: subcommand, |
| 86 | + ExitCode: int32(cmd.ExitCode), |
| 87 | + Usage: usage, |
| 88 | + StartTime: cmd.Start, |
| 89 | + DurationSecs: cmd.Duration.Seconds(), |
| 90 | + } |
| 91 | + |
| 92 | + return event |
| 93 | +} |
| 94 | + |
| 95 | +func findCommand(args []string) []*cmdNode { |
| 96 | + if len(args) == 0 { |
| 97 | + return nil |
| 98 | + } |
| 99 | + |
| 100 | + cmdPath := []*cmdNode{cmdHierarchy} |
| 101 | + if len(args) == 1 { |
| 102 | + return cmdPath |
| 103 | + } |
| 104 | + |
| 105 | + nodePath := []string{args[0]} |
| 106 | + for _, v := range args[1:] { |
| 107 | + v = strings.TrimSpace(v) |
| 108 | + if v == "" || strings.HasPrefix(v, "-") { |
| 109 | + continue |
| 110 | + } |
| 111 | + candidate := append(nodePath, v) |
| 112 | + if c := cmdHierarchy.find(candidate); c != nil { |
| 113 | + cmdPath = append(cmdPath, c) |
| 114 | + nodePath = candidate |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + return cmdPath |
| 119 | +} |
| 120 | + |
| 121 | +type cmdNode struct { |
| 122 | + name string |
| 123 | + plugin bool |
| 124 | + children []*cmdNode |
| 125 | +} |
| 126 | + |
| 127 | +func (c *cmdNode) find(path []string) *cmdNode { |
| 128 | + if len(path) == 0 { |
| 129 | + return nil |
| 130 | + } |
| 131 | + |
| 132 | + if c.name != path[0] { |
| 133 | + return nil |
| 134 | + } |
| 135 | + |
| 136 | + if len(path) == 1 { |
| 137 | + return c |
| 138 | + } |
| 139 | + |
| 140 | + remainder := path[1:] |
| 141 | + for _, child := range c.children { |
| 142 | + if res := child.find(remainder); res != nil { |
| 143 | + return res |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + return nil |
| 148 | +} |
| 149 | + |
| 150 | +var cmdHierarchy = &cmdNode{ |
| 151 | + name: "docker", |
| 152 | + children: []*cmdNode{ |
| 153 | + { |
| 154 | + name: "compose", |
| 155 | + plugin: true, |
| 156 | + children: []*cmdNode{ |
| 157 | + { |
| 158 | + name: "alpha", |
| 159 | + children: []*cmdNode{ |
| 160 | + {name: "watch"}, |
| 161 | + {name: "dryrun"}, |
| 162 | + }, |
| 163 | + }, |
| 164 | + {name: "build"}, |
| 165 | + {name: "config"}, |
| 166 | + {name: "convert"}, |
| 167 | + {name: "cp"}, |
| 168 | + {name: "create"}, |
| 169 | + {name: "down"}, |
| 170 | + {name: "events"}, |
| 171 | + {name: "exec"}, |
| 172 | + {name: "images"}, |
| 173 | + {name: "kill"}, |
| 174 | + {name: "logs"}, |
| 175 | + {name: "ls"}, |
| 176 | + {name: "pause"}, |
| 177 | + {name: "port"}, |
| 178 | + {name: "ps"}, |
| 179 | + {name: "pull"}, |
| 180 | + {name: "push"}, |
| 181 | + {name: "restart"}, |
| 182 | + {name: "rm"}, |
| 183 | + {name: "run"}, |
| 184 | + {name: "start"}, |
| 185 | + {name: "stop"}, |
| 186 | + {name: "top"}, |
| 187 | + {name: "unpause"}, |
| 188 | + {name: "up"}, |
| 189 | + {name: "version"}, |
| 190 | + }, |
| 191 | + }, |
| 192 | + }, |
| 193 | +} |
0 commit comments