1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
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"])
}
}
|