package main import ( "bufio" "bytes" "context" "io" "os/exec" "regexp" "strconv" "strings" "testing" "github.com/containerd/log" eventstestutils "github.com/docker/docker/daemon/events/testutils" "github.com/docker/docker/integration-cli/cli" "gotest.tools/v3/assert" ) // eventMatcher is a function that tries to match an event input. // It returns true if the event matches and a map with // a set of key/value to identify the match. type eventMatcher func(text string) (map[string]string, bool) // eventMatchProcessor is a function to handle an event match. // It receives a map of key/value with the information extracted in a match. type eventMatchProcessor func(matches map[string]string) // eventObserver runs an events commands and observes its output. type eventObserver struct { buffer *bytes.Buffer command *exec.Cmd scanner *bufio.Scanner startTime string disconnectionError error } // newEventObserver creates the observer and initializes the command // without running it. Users must call `eventObserver.Start` to start the command. func newEventObserver(c *testing.T, args ...string) (*eventObserver, error) { since := daemonTime(c).Unix() return newEventObserverWithBacklog(c, since, args...) } // newEventObserverWithBacklog creates a new observer changing the start time of the backlog to return. func newEventObserverWithBacklog(c *testing.T, since int64, args ...string) (*eventObserver, error) { startTime := strconv.FormatInt(since, 10) cmdArgs := []string{"events", "--since", startTime} if len(args) > 0 { cmdArgs = append(cmdArgs, args...) } eventsCmd := exec.Command(dockerBinary, cmdArgs...) stdout, err := eventsCmd.StdoutPipe() if err != nil { return nil, err } return &eventObserver{ buffer: new(bytes.Buffer), command: eventsCmd, scanner: bufio.NewScanner(stdout), startTime: startTime, }, nil } // Start starts the events command. func (e *eventObserver) Start() error { return e.command.Start() } // Stop stops the events command. func (e *eventObserver) Stop() { e.command.Process.Kill() e.command.Wait() } // Match tries to match the events output with a given matcher. func (e *eventObserver) Match(match eventMatcher, process eventMatchProcessor) { for e.scanner.Scan() { text := e.scanner.Text() e.buffer.WriteString(text) e.buffer.WriteString("\n") if matches, ok := match(text); ok { process(matches) } } err := e.scanner.Err() if err == nil { err = io.EOF } log.G(context.TODO()).Debugf("EventObserver scanner loop finished: %v", err) e.disconnectionError = err } func (e *eventObserver) CheckEventError(c *testing.T, id, event string, match eventMatcher) { var foundEvent bool scannerOut := e.buffer.String() if e.disconnectionError != nil { until := daemonUnixTime(c) out := cli.DockerCmd(c, "events", "--since", e.startTime, "--until", until).Stdout() events := strings.Split(strings.TrimSpace(out), "\n") for _, e := range events { if _, ok := match(e); ok { foundEvent = true break } } scannerOut = out } if !foundEvent { c.Fatalf("failed to observe event `%s` for %s. Disconnection error: %v\nout:\n%v", event, id, e.disconnectionError, scannerOut) } } // matchEventLine matches a text with the event regular expression. // It returns the matches and true if the regular expression matches with the given id and event type. // It returns an empty map and false if there is no match. func matchEventLine(id, eventType string, actions map[string]chan bool) eventMatcher { return func(text string) (map[string]string, bool) { matches := eventstestutils.ScanMap(text) if len(matches) == 0 { return matches, false } if matchIDAndEventType(matches, id, eventType) { if _, ok := actions[matches["action"]]; ok { return matches, true } } return matches, false } } // processEventMatch closes an action channel when an event line matches the expected action. func processEventMatch(actions map[string]chan bool) eventMatchProcessor { return func(matches map[string]string) { if ch, ok := actions[matches["action"]]; ok { ch <- true } } } // parseEventAction parses an event text and returns the action. // It fails if the text is not in the event format. func parseEventAction(c *testing.T, text string) string { matches := eventstestutils.ScanMap(text) return matches["action"] } // eventActionsByIDAndType returns the actions for a given id and type. // It fails if the text is not in the event format. func eventActionsByIDAndType(c *testing.T, events []string, id, eventType string) []string { var filtered []string for _, event := range events { matches := eventstestutils.ScanMap(event) assert.Assert(c, matches != nil) if matchIDAndEventType(matches, id, eventType) { filtered = append(filtered, matches["action"]) } } return filtered } // matchIDAndEventType returns true if an event matches a given id and type. // It also resolves names in the event attributes if the id doesn't match. func matchIDAndEventType(matches map[string]string, id, eventType string) bool { return matchEventID(matches, id) && matches["eventType"] == eventType } func matchEventID(matches map[string]string, id string) bool { matchID := matches["id"] == id || strings.HasPrefix(matches["id"], id) if !matchID && matches["attributes"] != "" { // try matching a name in the attributes attributes := map[string]string{} for _, a := range strings.Split(matches["attributes"], ", ") { kv := strings.Split(a, "=") attributes[kv[0]] = kv[1] } matchID = attributes["name"] == id } return matchID } func parseEvents(c *testing.T, out, match string) { events := strings.Split(strings.TrimSpace(out), "\n") for _, event := range events { matches := eventstestutils.ScanMap(event) matched, err := regexp.MatchString(match, matches["action"]) assert.NilError(c, err) assert.Assert(c, matched, "Matcher: %s did not match %s", match, matches["action"]) } }