github.com/uriddle/docker@v0.0.0-20210926094723-4072e6aeb013/integration-cli/events_utils.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io" 8 "os/exec" 9 "regexp" 10 "strconv" 11 "strings" 12 13 "github.com/Sirupsen/logrus" 14 "github.com/docker/docker/pkg/integration/checker" 15 "github.com/go-check/check" 16 ) 17 18 var ( 19 reTimestamp = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{9}(:?(:?(:?-|\+)\d{2}:\d{2})|Z)` 20 reEventType = `(?P<eventType>\w+)` 21 reAction = `(?P<action>\w+)` 22 reID = `(?P<id>[^\s]+)` 23 reAttributes = `(\s\((?P<attributes>[^\)]+)\))?` 24 reString = fmt.Sprintf(`\A%s\s%s\s%s\s%s%s\z`, reTimestamp, reEventType, reAction, reID, reAttributes) 25 26 // eventCliRegexp is a regular expression that matches all possible event outputs in the cli 27 eventCliRegexp = regexp.MustCompile(reString) 28 ) 29 30 // eventMatcher is a function that tries to match an event input. 31 // It returns true if the event matches and a map with 32 // a set of key/value to identify the match. 33 type eventMatcher func(text string) (map[string]string, bool) 34 35 // eventMatchProcessor is a function to handle an event match. 36 // It receives a map of key/value with the information extracted in a match. 37 type eventMatchProcessor func(matches map[string]string) 38 39 // eventObserver runs an events commands and observes its output. 40 type eventObserver struct { 41 buffer *bytes.Buffer 42 command *exec.Cmd 43 scanner *bufio.Scanner 44 startTime string 45 disconnectionError error 46 } 47 48 // newEventObserver creates the observer and initializes the command 49 // without running it. Users must call `eventObserver.Start` to start the command. 50 func newEventObserver(c *check.C, args ...string) (*eventObserver, error) { 51 since := daemonTime(c).Unix() 52 return newEventObserverWithBacklog(c, since, args...) 53 } 54 55 // newEventObserverWithBacklog creates a new observer changing the start time of the backlog to return. 56 func newEventObserverWithBacklog(c *check.C, since int64, args ...string) (*eventObserver, error) { 57 startTime := strconv.FormatInt(since, 10) 58 cmdArgs := []string{"events", "--since", startTime} 59 if len(args) > 0 { 60 cmdArgs = append(cmdArgs, args...) 61 } 62 eventsCmd := exec.Command(dockerBinary, cmdArgs...) 63 stdout, err := eventsCmd.StdoutPipe() 64 if err != nil { 65 return nil, err 66 } 67 68 return &eventObserver{ 69 buffer: new(bytes.Buffer), 70 command: eventsCmd, 71 scanner: bufio.NewScanner(stdout), 72 startTime: startTime, 73 }, nil 74 } 75 76 // Start starts the events command. 77 func (e *eventObserver) Start() error { 78 return e.command.Start() 79 } 80 81 // Stop stops the events command. 82 func (e *eventObserver) Stop() { 83 e.command.Process.Kill() 84 e.command.Process.Release() 85 } 86 87 // Match tries to match the events output with a given matcher. 88 func (e *eventObserver) Match(match eventMatcher, process eventMatchProcessor) { 89 for e.scanner.Scan() { 90 text := e.scanner.Text() 91 e.buffer.WriteString(text) 92 e.buffer.WriteString("\n") 93 94 if matches, ok := match(text); ok { 95 process(matches) 96 } 97 } 98 99 err := e.scanner.Err() 100 if err == nil { 101 err = io.EOF 102 } 103 104 logrus.Debug("EventObserver scanner loop finished: %v", err) 105 e.disconnectionError = err 106 } 107 108 func (e *eventObserver) CheckEventError(c *check.C, id, event string, match eventMatcher) { 109 var foundEvent bool 110 scannerOut := e.buffer.String() 111 112 if e.disconnectionError != nil { 113 until := strconv.FormatInt(daemonTime(c).Unix(), 10) 114 out, _ := dockerCmd(c, "events", "--since", e.startTime, "--until", until) 115 events := strings.Split(strings.TrimSpace(out), "\n") 116 for _, e := range events { 117 if _, ok := match(e); ok { 118 foundEvent = true 119 break 120 } 121 } 122 scannerOut = out 123 } 124 if !foundEvent { 125 c.Fatalf("failed to observe event `%s` for %s. Disconnection error: %v\nout:\n%v", event, id, e.disconnectionError, scannerOut) 126 } 127 } 128 129 // matchEventLine matches a text with the event regular expression. 130 // It returns the matches and true if the regular expression matches with the given id and event type. 131 // It returns an empty map and false if there is no match. 132 func matchEventLine(id, eventType string, actions map[string]chan bool) eventMatcher { 133 return func(text string) (map[string]string, bool) { 134 matches := parseEventText(text) 135 if len(matches) == 0 { 136 return matches, false 137 } 138 139 if matchIDAndEventType(matches, id, eventType) { 140 if _, ok := actions[matches["action"]]; ok { 141 return matches, true 142 } 143 } 144 return matches, false 145 } 146 } 147 148 // processEventMatch closes an action channel when an event line matches the expected action. 149 func processEventMatch(actions map[string]chan bool) eventMatchProcessor { 150 return func(matches map[string]string) { 151 if ch, ok := actions[matches["action"]]; ok { 152 close(ch) 153 } 154 } 155 } 156 157 // parseEventText parses a line of events coming from the cli and returns 158 // the matchers in a map. 159 func parseEventText(text string) map[string]string { 160 matches := eventCliRegexp.FindAllStringSubmatch(text, -1) 161 md := map[string]string{} 162 if len(matches) == 0 { 163 return md 164 } 165 166 names := eventCliRegexp.SubexpNames() 167 for i, n := range matches[0] { 168 md[names[i]] = n 169 } 170 return md 171 } 172 173 // parseEventAction parses an event text and returns the action. 174 // It fails if the text is not in the event format. 175 func parseEventAction(c *check.C, text string) string { 176 matches := parseEventText(text) 177 return matches["action"] 178 } 179 180 // eventActionsByIDAndType returns the actions for a given id and type. 181 // It fails if the text is not in the event format. 182 func eventActionsByIDAndType(c *check.C, events []string, id, eventType string) []string { 183 var filtered []string 184 for _, event := range events { 185 matches := parseEventText(event) 186 c.Assert(matches, checker.Not(checker.IsNil)) 187 if matchIDAndEventType(matches, id, eventType) { 188 filtered = append(filtered, matches["action"]) 189 } 190 } 191 return filtered 192 } 193 194 // matchIDAndEventType returns true if an event matches a given id and type. 195 // It also resolves names in the event attributes if the id doesn't match. 196 func matchIDAndEventType(matches map[string]string, id, eventType string) bool { 197 return matchEventID(matches, id) && matches["eventType"] == eventType 198 } 199 200 func matchEventID(matches map[string]string, id string) bool { 201 matchID := matches["id"] == id || strings.HasPrefix(matches["id"], id) 202 if !matchID && matches["attributes"] != "" { 203 // try matching a name in the attributes 204 attributes := map[string]string{} 205 for _, a := range strings.Split(matches["attributes"], ", ") { 206 kv := strings.Split(a, "=") 207 attributes[kv[0]] = kv[1] 208 } 209 matchID = attributes["name"] == id 210 } 211 return matchID 212 } 213 214 func parseEvents(c *check.C, out, match string) { 215 events := strings.Split(strings.TrimSpace(out), "\n") 216 for _, event := range events { 217 matches := parseEventText(event) 218 matched, err := regexp.MatchString(match, matches["action"]) 219 c.Assert(err, checker.IsNil) 220 c.Assert(matched, checker.True, check.Commentf("Matcher: %s did not match %s", match, matches["action"])) 221 } 222 } 223 224 func parseEventsWithID(c *check.C, out, match, id string) { 225 events := strings.Split(strings.TrimSpace(out), "\n") 226 for _, event := range events { 227 matches := parseEventText(event) 228 c.Assert(matchEventID(matches, id), checker.True) 229 230 matched, err := regexp.MatchString(match, matches["action"]) 231 c.Assert(err, checker.IsNil) 232 c.Assert(matched, checker.True, check.Commentf("Matcher: %s did not match %s", match, matches["action"])) 233 } 234 }