From 72f1881df102fce9ad31e98045b91c204dd44513 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Mon, 21 Dec 2015 17:55:23 -0500 Subject: [PATCH] Add event types. - Stop serializing JSONMessage in favor of events.Message. - Keep backwards compatibility with JSONMessage for container events. Signed-off-by: David Calavera --- api/client/events.go | 49 ++++- api/server/router/system/backend.go | 4 +- api/server/router/system/system_routes.go | 5 +- api/types/events/events.go | 38 ++++ api/types/filters/parse.go | 16 ++ api/types/filters/parse_test.go | 18 ++ daemon/daemon.go | 78 +++---- daemon/events.go | 78 ++++++- daemon/events/events.go | 42 ++-- daemon/events/events_test.go | 35 +++- daemon/events/filter.go | 46 +++-- daemon/events_test.go | 36 ++++ daemon/image_delete.go | 8 +- daemon/import.go | 2 +- daemon/update.go | 2 + distribution/pull.go | 7 +- distribution/push.go | 7 +- integration-cli/docker_api_events_test.go | 43 ++++ integration-cli/docker_cli_build_test.go | 73 +++---- integration-cli/docker_cli_events_test.go | 191 ++++++++---------- .../docker_cli_events_unix_test.go | 54 +++-- integration-cli/docker_cli_pause_test.go | 31 +-- integration-cli/events_utils.go | 119 +++++++++++ 23 files changed, 665 insertions(+), 317 deletions(-) create mode 100644 api/types/events/events.go create mode 100644 daemon/events_test.go diff --git a/api/client/events.go b/api/client/events.go index ba0227d8fb..14f0f6e9bd 100644 --- a/api/client/events.go +++ b/api/client/events.go @@ -1,11 +1,18 @@ package client import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + "github.com/docker/docker/api/types" + eventtypes "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/jsonlog" flag "github.com/docker/docker/pkg/mflag" ) @@ -46,5 +53,43 @@ func (cli *DockerCli) CmdEvents(args ...string) error { } defer responseBody.Close() - return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) + return streamEvents(responseBody, cli.out) +} + +// streamEvents decodes prints the incoming events in the provided output. +func streamEvents(input io.Reader, output io.Writer) error { + dec := json.NewDecoder(input) + for { + var event eventtypes.Message + if err := dec.Decode(&event); err != nil { + if err == io.EOF { + break + } + return err + } + printOutput(event, output) + } + return nil +} + +// printOutput prints all types of event information. +// Each output includes the event type, actor id, name and action. +// Actor attributes are printed at the end if the actor has any. +func printOutput(event eventtypes.Message, output io.Writer) { + if event.TimeNano != 0 { + fmt.Fprintf(output, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) + } else if event.Time != 0 { + fmt.Fprintf(output, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) + } + + fmt.Fprintf(output, "%s %s %s", event.Type, event.Action, event.Actor.ID) + + if len(event.Actor.Attributes) > 0 { + var attrs []string + for k, v := range event.Actor.Attributes { + attrs = append(attrs, fmt.Sprintf("%s=%s", k, v)) + } + fmt.Fprintf(output, " (%s)", strings.Join(attrs, ", ")) + } + fmt.Fprint(output, "\n") } diff --git a/api/server/router/system/backend.go b/api/server/router/system/backend.go index 21755b78e5..bc218087d9 100644 --- a/api/server/router/system/backend.go +++ b/api/server/router/system/backend.go @@ -2,8 +2,8 @@ package system import ( "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/jsonmessage" ) // Backend is the methods that need to be implemented to provide @@ -11,7 +11,7 @@ import ( type Backend interface { SystemInfo() (*types.Info, error) SystemVersion() types.Version - SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]*jsonmessage.JSONMessage, chan interface{}) + SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{}) UnsubscribeFromEvents(chan interface{}) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error) } diff --git a/api/server/router/system/system_routes.go b/api/server/router/system/system_routes.go index d5fd655980..cef2fb3942 100644 --- a/api/server/router/system/system_routes.go +++ b/api/server/router/system/system_routes.go @@ -9,10 +9,10 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" timetypes "github.com/docker/docker/api/types/time" "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/jsonmessage" "golang.org/x/net/context" ) @@ -98,8 +98,9 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r * for { select { case ev := <-l: - jev, ok := ev.(*jsonmessage.JSONMessage) + jev, ok := ev.(events.Message) if !ok { + logrus.Warnf("unexpected event message: %q", ev) continue } if err := enc.Encode(jev); err != nil { diff --git a/api/types/events/events.go b/api/types/events/events.go new file mode 100644 index 0000000000..c5987aaf14 --- /dev/null +++ b/api/types/events/events.go @@ -0,0 +1,38 @@ +package events + +const ( + // ContainerEventType is the event type that containers generate + ContainerEventType = "container" + // ImageEventType is the event type that images generate + ImageEventType = "image" + // VolumeEventType is the event type that volumes generate + VolumeEventType = "volume" + // NetworkEventType is the event type that networks generate + NetworkEventType = "network" +) + +// Actor describes something that generates events, +// like a container, or a network, or a volume. +// It has a defined name and a set or attributes. +// The container attributes are its labels, other actors +// can generate these attributes from other properties. +type Actor struct { + ID string + Attributes map[string]string +} + +// Message represents the information an event contains +type Message struct { + // Deprecated information from JSONMessage. + // With data only in container events. + Status string `json:"status,omitempty"` + ID string `json:"id,omitempty"` + From string `json:"from,omitempty"` + + Type string + Action string + Actor Actor + + Time int64 `json:"time,omitempty"` + TimeNano int64 `json:"timeNano,omitempty"` +} diff --git a/api/types/filters/parse.go b/api/types/filters/parse.go index 6c423ceae8..e99462c0a8 100644 --- a/api/types/filters/parse.go +++ b/api/types/filters/parse.go @@ -197,6 +197,22 @@ func (filters Args) ExactMatch(field, source string) bool { return false } +// FuzzyMatch returns true if the source matches exactly one of the filters, +// or the source has one of the filters as a prefix. +func (filters Args) FuzzyMatch(field, source string) bool { + if filters.ExactMatch(field, source) { + return true + } + + fieldValues := filters.fields[field] + for prefix := range fieldValues { + if strings.HasPrefix(source, prefix) { + return true + } + } + return false +} + // Include returns true if the name of the field to filter is in the filters. func (filters Args) Include(field string) bool { _, ok := filters.fields[field] diff --git a/api/types/filters/parse_test.go b/api/types/filters/parse_test.go index 308d1bcdb8..4d5c814551 100644 --- a/api/types/filters/parse_test.go +++ b/api/types/filters/parse_test.go @@ -349,3 +349,21 @@ func TestWalkValues(t *testing.T) { t.Fatalf("Expected to not iterate when the field doesn't exist, got %v", err) } } + +func TestFuzzyMatch(t *testing.T) { + f := NewArgs() + f.Add("container", "foo") + + cases := map[string]bool{ + "foo": true, + "foobar": true, + "barfoo": false, + "bar": false, + } + for source, match := range cases { + got := f.FuzzyMatch("container", source) + if got != match { + t.Fatalf("Expected %v, got %v: %s", match, got, source) + } + } +} diff --git a/daemon/daemon.go b/daemon/daemon.go index 70633debba..d7ca8ca1b9 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -22,6 +22,7 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" + eventtypes "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/strslice" @@ -47,7 +48,6 @@ import ( "github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/graphdb" "github.com/docker/docker/pkg/idtools" - "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/namesgenerator" "github.com/docker/docker/pkg/progress" @@ -554,23 +554,9 @@ func (daemon *Daemon) GetByName(name string) (*container.Container, error) { return e, nil } -// getEventFilter returns a filters.Filter for a set of filters -func (daemon *Daemon) getEventFilter(filter filters.Args) *events.Filter { - // incoming container filter can be name, id or partial id, convert to - // a full container id - for _, cn := range filter.Get("container") { - c, err := daemon.GetContainer(cn) - filter.Del("container", cn) - if err == nil { - filter.Add("container", c.ID) - } - } - return events.NewFilter(filter, daemon.GetLabels) -} - // SubscribeToEvents returns the currently record of events, a channel to stream new events from, and a function to cancel the stream of events. -func (daemon *Daemon) SubscribeToEvents(since, sinceNano int64, filter filters.Args) ([]*jsonmessage.JSONMessage, chan interface{}) { - ef := daemon.getEventFilter(filter) +func (daemon *Daemon) SubscribeToEvents(since, sinceNano int64, filter filters.Args) ([]eventtypes.Message, chan interface{}) { + ef := events.NewFilter(filter) return daemon.EventsService.SubscribeTopic(since, sinceNano, ef) } @@ -580,21 +566,6 @@ func (daemon *Daemon) UnsubscribeFromEvents(listener chan interface{}) { daemon.EventsService.Evict(listener) } -// GetLabels for a container or image id -func (daemon *Daemon) GetLabels(id string) map[string]string { - // TODO: TestCase - container := daemon.containers.Get(id) - if container != nil { - return container.Config.Labels - } - - img, err := daemon.GetImage(id) - if err == nil { - return img.ContainerConfig.Labels - } - return nil -} - // children returns all child containers of the container with the // given name. The containers are returned as a map from the container // name to a pointer to Container. @@ -1032,7 +1003,8 @@ func (daemon *Daemon) TagImage(newTag reference.Named, imageName string) error { if err := daemon.referenceStore.AddTag(newTag, imageID, true); err != nil { return err } - daemon.EventsService.Log("tag", newTag.String(), "") + + daemon.LogImageEvent(imageID.String(), newTag.String(), "tag") return nil } @@ -1068,15 +1040,15 @@ func (daemon *Daemon) PullImage(ref reference.Named, metaHeaders map[string][]st }() imagePullConfig := &distribution.ImagePullConfig{ - MetaHeaders: metaHeaders, - AuthConfig: authConfig, - ProgressOutput: progress.ChanOutput(progressChan), - RegistryService: daemon.RegistryService, - EventsService: daemon.EventsService, - MetadataStore: daemon.distributionMetadataStore, - ImageStore: daemon.imageStore, - ReferenceStore: daemon.referenceStore, - DownloadManager: daemon.downloadManager, + MetaHeaders: metaHeaders, + AuthConfig: authConfig, + ProgressOutput: progress.ChanOutput(progressChan), + RegistryService: daemon.RegistryService, + ImageEventLogger: daemon.LogImageEvent, + MetadataStore: daemon.distributionMetadataStore, + ImageStore: daemon.imageStore, + ReferenceStore: daemon.referenceStore, + DownloadManager: daemon.downloadManager, } err := distribution.Pull(ctx, ref, imagePullConfig) @@ -1111,17 +1083,17 @@ func (daemon *Daemon) PushImage(ref reference.Named, metaHeaders map[string][]st }() imagePushConfig := &distribution.ImagePushConfig{ - MetaHeaders: metaHeaders, - AuthConfig: authConfig, - ProgressOutput: progress.ChanOutput(progressChan), - RegistryService: daemon.RegistryService, - EventsService: daemon.EventsService, - MetadataStore: daemon.distributionMetadataStore, - LayerStore: daemon.layerStore, - ImageStore: daemon.imageStore, - ReferenceStore: daemon.referenceStore, - TrustKey: daemon.trustKey, - UploadManager: daemon.uploadManager, + MetaHeaders: metaHeaders, + AuthConfig: authConfig, + ProgressOutput: progress.ChanOutput(progressChan), + RegistryService: daemon.RegistryService, + ImageEventLogger: daemon.LogImageEvent, + MetadataStore: daemon.distributionMetadataStore, + LayerStore: daemon.layerStore, + ImageStore: daemon.imageStore, + ReferenceStore: daemon.referenceStore, + TrustKey: daemon.trustKey, + UploadManager: daemon.uploadManager, } err := distribution.Push(ctx, ref, imagePushConfig) diff --git a/daemon/events.go b/daemon/events.go index 969e1d3964..cee57631c3 100644 --- a/daemon/events.go +++ b/daemon/events.go @@ -1,14 +1,82 @@ package daemon import ( + "strings" + + "github.com/docker/docker/api/types/events" "github.com/docker/docker/container" + "github.com/docker/libnetwork" ) // LogContainerEvent generates an event related to a container. func (daemon *Daemon) LogContainerEvent(container *container.Container, action string) { - daemon.EventsService.Log( - action, - container.ID, - container.Config.Image, - ) + attributes := copyAttributes(container.Config.Labels) + if container.Config.Image != "" { + attributes["image"] = container.Config.Image + } + attributes["name"] = strings.TrimLeft(container.Name, "/") + + actor := events.Actor{ + ID: container.ID, + Attributes: attributes, + } + daemon.EventsService.Log(action, events.ContainerEventType, actor) +} + +// LogImageEvent generates an event related to a container. +func (daemon *Daemon) LogImageEvent(imageID, refName, action string) { + attributes := map[string]string{} + img, err := daemon.GetImage(imageID) + if err == nil && img.Config != nil { + // image has not been removed yet. + // it could be missing if the event is `delete`. + attributes = copyAttributes(img.Config.Labels) + } + if refName != "" { + attributes["name"] = refName + } + actor := events.Actor{ + ID: imageID, + Attributes: attributes, + } + + daemon.EventsService.Log(action, events.ImageEventType, actor) +} + +// LogVolumeEvent generates an event related to a volume. +func (daemon *Daemon) LogVolumeEvent(volumeID, action string, attributes map[string]string) { + actor := events.Actor{ + ID: volumeID, + Attributes: attributes, + } + daemon.EventsService.Log(action, events.VolumeEventType, actor) +} + +// LogNetworkEvent generates an event related to a network with only the default attributes. +func (daemon *Daemon) LogNetworkEvent(nw libnetwork.Network, action string) { + daemon.LogNetworkEventWithAttributes(nw, action, map[string]string{}) +} + +// LogNetworkEventWithAttributes generates an event related to a network with specific given attributes. +func (daemon *Daemon) LogNetworkEventWithAttributes(nw libnetwork.Network, action string, attributes map[string]string) { + attributes["name"] = nw.Name() + attributes["type"] = nw.Type() + + actor := events.Actor{ + ID: nw.ID(), + Attributes: attributes, + } + daemon.EventsService.Log(action, events.NetworkEventType, actor) +} + +// copyAttributes guarantees that labels are not mutated by event triggers. +func copyAttributes(labels map[string]string) map[string]string { + attributes := map[string]string{} + if labels == nil { + return attributes + } + for k, v := range labels { + attributes[k] = v + } + return attributes } diff --git a/daemon/events/events.go b/daemon/events/events.go index 3674170fe3..6f44e7c500 100644 --- a/daemon/events/events.go +++ b/daemon/events/events.go @@ -4,7 +4,7 @@ import ( "sync" "time" - "github.com/docker/docker/pkg/jsonmessage" + eventtypes "github.com/docker/docker/api/types/events" "github.com/docker/docker/pkg/pubsub" ) @@ -13,17 +13,17 @@ const ( bufferSize = 1024 ) -// Events is pubsub channel for *jsonmessage.JSONMessage +// Events is pubsub channel for events generated by the engine. type Events struct { mu sync.Mutex - events []*jsonmessage.JSONMessage + events []eventtypes.Message pub *pubsub.Publisher } // New returns new *Events instance func New() *Events { return &Events{ - events: make([]*jsonmessage.JSONMessage, 0, eventsLimit), + events: make([]eventtypes.Message, 0, eventsLimit), pub: pubsub.NewPublisher(100*time.Millisecond, bufferSize), } } @@ -32,9 +32,9 @@ func New() *Events { // last events, a channel in which you can expect new events (in form // of interface{}, so you need type assertion), and a function to call // to stop the stream of events. -func (e *Events) Subscribe() ([]*jsonmessage.JSONMessage, chan interface{}, func()) { +func (e *Events) Subscribe() ([]eventtypes.Message, chan interface{}, func()) { e.mu.Lock() - current := make([]*jsonmessage.JSONMessage, len(e.events)) + current := make([]eventtypes.Message, len(e.events)) copy(current, e.events) l := e.pub.Subscribe() e.mu.Unlock() @@ -48,13 +48,13 @@ func (e *Events) Subscribe() ([]*jsonmessage.JSONMessage, chan interface{}, func // SubscribeTopic adds new listener to events, returns slice of 64 stored // last events, a channel in which you can expect new events (in form // of interface{}, so you need type assertion). -func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]*jsonmessage.JSONMessage, chan interface{}) { +func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]eventtypes.Message, chan interface{}) { e.mu.Lock() defer e.mu.Unlock() - var buffered []*jsonmessage.JSONMessage + var buffered []eventtypes.Message topic := func(m interface{}) bool { - return ef.Include(m.(*jsonmessage.JSONMessage)) + return ef.Include(m.(eventtypes.Message)) } if since != -1 { @@ -64,7 +64,7 @@ func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]*jsonmess break } if ef.filter.Len() == 0 || topic(ev) { - buffered = append([]*jsonmessage.JSONMessage{ev}, buffered...) + buffered = append([]eventtypes.Message{ev}, buffered...) } } } @@ -87,9 +87,27 @@ func (e *Events) Evict(l chan interface{}) { // Log broadcasts event to listeners. Each listener has 100 millisecond for // receiving event or it will be skipped. -func (e *Events) Log(action, id, from string) { +func (e *Events) Log(action, eventType string, actor eventtypes.Actor) { now := time.Now().UTC() - jm := &jsonmessage.JSONMessage{Status: action, ID: id, From: from, Time: now.Unix(), TimeNano: now.UnixNano()} + jm := eventtypes.Message{ + Action: action, + Type: eventType, + Actor: actor, + Time: now.Unix(), + TimeNano: now.UnixNano(), + } + + // fill deprecated fields for container and images + switch eventType { + case eventtypes.ContainerEventType: + jm.ID = actor.ID + jm.Status = action + jm.From = actor.Attributes["image"] + case eventtypes.ImageEventType: + jm.ID = actor.ID + jm.Status = action + } + e.mu.Lock() if len(e.events) == cap(e.events) { // discard oldest event diff --git a/daemon/events/events_test.go b/daemon/events/events_test.go index cf3c28aac5..a782636370 100644 --- a/daemon/events/events_test.go +++ b/daemon/events/events_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/api/types/events" ) func TestEventsLog(t *testing.T) { @@ -18,10 +18,14 @@ func TestEventsLog(t *testing.T) { if count != 2 { t.Fatalf("Must be 2 subscribers, got %d", count) } - e.Log("test", "cont", "image") + actor := events.Actor{ + ID: "cont", + Attributes: map[string]string{"image": "image"}, + } + e.Log("test", events.ContainerEventType, actor) select { case msg := <-l1: - jmsg, ok := msg.(*jsonmessage.JSONMessage) + jmsg, ok := msg.(events.Message) if !ok { t.Fatalf("Unexpected type %T", msg) } @@ -42,7 +46,7 @@ func TestEventsLog(t *testing.T) { } select { case msg := <-l2: - jmsg, ok := msg.(*jsonmessage.JSONMessage) + jmsg, ok := msg.(events.Message) if !ok { t.Fatalf("Unexpected type %T", msg) } @@ -70,7 +74,10 @@ func TestEventsLogTimeout(t *testing.T) { c := make(chan struct{}) go func() { - e.Log("test", "cont", "image") + actor := events.Actor{ + ID: "image", + } + e.Log("test", events.ImageEventType, actor) close(c) }() @@ -88,7 +95,12 @@ func TestLogEvents(t *testing.T) { action := fmt.Sprintf("action_%d", i) id := fmt.Sprintf("cont_%d", i) from := fmt.Sprintf("image_%d", i) - e.Log(action, id, from) + + actor := events.Actor{ + ID: id, + Attributes: map[string]string{"image": from}, + } + e.Log(action, events.ContainerEventType, actor) } time.Sleep(50 * time.Millisecond) current, l, _ := e.Subscribe() @@ -97,16 +109,21 @@ func TestLogEvents(t *testing.T) { action := fmt.Sprintf("action_%d", num) id := fmt.Sprintf("cont_%d", num) from := fmt.Sprintf("image_%d", num) - e.Log(action, id, from) + + actor := events.Actor{ + ID: id, + Attributes: map[string]string{"image": from}, + } + e.Log(action, events.ContainerEventType, actor) } if len(e.events) != eventsLimit { t.Fatalf("Must be %d events, got %d", eventsLimit, len(e.events)) } - var msgs []*jsonmessage.JSONMessage + var msgs []events.Message for len(msgs) < 10 { m := <-l - jm, ok := (m).(*jsonmessage.JSONMessage) + jm, ok := (m).(events.Message) if !ok { t.Fatalf("Unexpected type %T", m) } diff --git a/daemon/events/filter.go b/daemon/events/filter.go index 084346cfff..0d690eed31 100644 --- a/daemon/events/filter.go +++ b/daemon/events/filter.go @@ -1,46 +1,58 @@ package events import ( + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" ) // Filter can filter out docker events from a stream type Filter struct { - filter filters.Args - getLabels func(id string) map[string]string + filter filters.Args } // NewFilter creates a new Filter -func NewFilter(filter filters.Args, getLabels func(id string) map[string]string) *Filter { - return &Filter{filter: filter, getLabels: getLabels} +func NewFilter(filter filters.Args) *Filter { + return &Filter{filter: filter} } // Include returns true when the event ev is included by the filters -func (ef *Filter) Include(ev *jsonmessage.JSONMessage) bool { - return ef.filter.ExactMatch("event", ev.Status) && - ef.filter.ExactMatch("container", ev.ID) && - ef.isImageIncluded(ev.ID, ev.From) && - ef.isLabelFieldIncluded(ev.ID) +func (ef *Filter) Include(ev events.Message) bool { + if ev.Type != events.ContainerEventType && ev.Type != events.ImageEventType { + return false + } + return ef.filter.ExactMatch("event", ev.Action) && + ef.matchContainer(ev) && + ef.isImageIncluded(ev) && + ef.isLabelFieldIncluded(ev.Actor.Attributes) } -func (ef *Filter) isLabelFieldIncluded(id string) bool { +func (ef *Filter) isLabelFieldIncluded(attributes map[string]string) bool { if !ef.filter.Include("label") { return true } - return ef.filter.MatchKVList("label", ef.getLabels(id)) + return ef.filter.MatchKVList("label", attributes) +} + +func (ef *Filter) matchContainer(ev events.Message) bool { + return ef.filter.FuzzyMatch("container", ev.Actor.ID) || + ef.filter.FuzzyMatch("container", ev.Actor.Attributes["name"]) } // The image filter will be matched against both event.ID (for image events) // and event.From (for container events), so that any container that was created // from an image will be included in the image events. Also compare both // against the stripped repo name without any tags. -func (ef *Filter) isImageIncluded(eventID string, eventFrom string) bool { - return ef.filter.ExactMatch("image", eventID) || - ef.filter.ExactMatch("image", eventFrom) || - ef.filter.ExactMatch("image", stripTag(eventID)) || - ef.filter.ExactMatch("image", stripTag(eventFrom)) +func (ef *Filter) isImageIncluded(ev events.Message) bool { + id := ev.ID + var imageName string + if n, ok := ev.Actor.Attributes["image"]; ok { + imageName = n + } + return ef.filter.ExactMatch("image", id) || + ef.filter.ExactMatch("image", imageName) || + ef.filter.ExactMatch("image", stripTag(id)) || + ef.filter.ExactMatch("image", stripTag(imageName)) } func stripTag(image string) string { diff --git a/daemon/events_test.go b/daemon/events_test.go new file mode 100644 index 0000000000..7035ebd06b --- /dev/null +++ b/daemon/events_test.go @@ -0,0 +1,36 @@ +package daemon + +import ( + "testing" + + containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/events" +) + +func TestLogContainerCopyLabels(t *testing.T) { + e := events.New() + _, l, _ := e.Subscribe() + defer e.Evict(l) + + container := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "container_id", + Name: "container_name", + Config: &containertypes.Config{ + Labels: map[string]string{ + "node": "1", + "os": "alpine", + }, + }, + }, + } + daemon := &Daemon{ + EventsService: e, + } + daemon.LogContainerEvent(container, "create") + + if _, mutated := container.Config.Labels["image"]; mutated { + t.Fatalf("Expected to not mutate the container labels, got %q", container.Config.Labels) + } +} diff --git a/daemon/image_delete.go b/daemon/image_delete.go index ab8217b5e1..b2aacfb606 100644 --- a/daemon/image_delete.go +++ b/daemon/image_delete.go @@ -87,7 +87,7 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} - daemon.EventsService.Log("untag", imgID.String(), "") + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") records = append(records, untaggedRecord) // If has remaining references then untag finishes the remove @@ -109,7 +109,7 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} - daemon.EventsService.Log("untag", imgID.String(), "") + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") records = append(records, untaggedRecord) } } @@ -174,7 +174,7 @@ func (daemon *Daemon) removeAllReferencesToImageID(imgID image.ID, records *[]ty untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} - daemon.EventsService.Log("untag", imgID.String(), "") + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") *records = append(*records, untaggedRecord) } @@ -243,7 +243,7 @@ func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDe return err } - daemon.EventsService.Log("delete", imgID.String(), "") + daemon.LogImageEvent(imgID.String(), imgID.String(), "delete") *records = append(*records, types.ImageDelete{Deleted: imgID.String()}) for _, removedLayer := range removedLayers { *records = append(*records, types.ImageDelete{Deleted: removedLayer.ChainID.String()}) diff --git a/daemon/import.go b/daemon/import.go index e74f2cf962..80d2146c23 100644 --- a/daemon/import.go +++ b/daemon/import.go @@ -97,7 +97,7 @@ func (daemon *Daemon) ImportImage(src string, newRef reference.Named, msg string } } - daemon.EventsService.Log("import", id.String(), "") + daemon.LogImageEvent(id.String(), id.String(), "import") outStream.Write(sf.FormatStatus("", id.String())) return nil } diff --git a/daemon/update.go b/daemon/update.go index c1e55241aa..5699901e75 100644 --- a/daemon/update.go +++ b/daemon/update.go @@ -54,5 +54,7 @@ func (daemon *Daemon) update(name string, hostConfig *container.HostConfig) erro } } + daemon.LogContainerEvent(container, "update") + return nil } diff --git a/distribution/pull.go b/distribution/pull.go index 0466ed6146..ca260d94a8 100644 --- a/distribution/pull.go +++ b/distribution/pull.go @@ -7,7 +7,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" - "github.com/docker/docker/daemon/events" "github.com/docker/docker/distribution/metadata" "github.com/docker/docker/distribution/xfer" "github.com/docker/docker/image" @@ -31,8 +30,8 @@ type ImagePullConfig struct { // RegistryService is the registry service to use for TLS configuration // and endpoint lookup. RegistryService *registry.Service - // EventsService is the events service to use for logging. - EventsService *events.Events + // ImageEventLogger notifies events for a given image + ImageEventLogger func(id, name, action string) // MetadataStore is the storage backend for distribution-specific // metadata. MetadataStore metadata.Store @@ -160,7 +159,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo } } - imagePullConfig.EventsService.Log("pull", ref.String(), "") + imagePullConfig.ImageEventLogger(ref.String(), repoInfo.Name(), "pull") return nil } diff --git a/distribution/push.go b/distribution/push.go index fb401f4f21..9c49d40857 100644 --- a/distribution/push.go +++ b/distribution/push.go @@ -9,7 +9,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" "github.com/docker/docker/api/types" - "github.com/docker/docker/daemon/events" "github.com/docker/docker/distribution/metadata" "github.com/docker/docker/distribution/xfer" "github.com/docker/docker/image" @@ -35,8 +34,8 @@ type ImagePushConfig struct { // RegistryService is the registry service to use for TLS configuration // and endpoint lookup. RegistryService *registry.Service - // EventsService is the events service to use for logging. - EventsService *events.Events + // ImageEventLogger notifies events for a given image + ImageEventLogger func(id, name, action string) // MetadataStore is the storage backend for distribution-specific // metadata. MetadataStore metadata.Store @@ -156,7 +155,7 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo return err } - imagePushConfig.EventsService.Log("push", repoInfo.Name(), "") + imagePushConfig.ImageEventLogger(ref.String(), repoInfo.Name(), "push") return nil } diff --git a/integration-cli/docker_api_events_test.go b/integration-cli/docker_api_events_test.go index d6f2609f89..5d6e817f02 100644 --- a/integration-cli/docker_api_events_test.go +++ b/integration-cli/docker_api_events_test.go @@ -1,10 +1,16 @@ package main import ( + "encoding/json" + "io" "net/http" + "net/url" + "strconv" + "strings" "time" "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/jsonmessage" "github.com/go-check/check" ) @@ -28,3 +34,40 @@ func (s *DockerSuite) TestEventsApiEmptyOutput(c *check.C) { c.Fatal("timeout waiting for events api to respond, should have responded immediately") } } + +func (s *DockerSuite) TestEventsApiBackwardsCompatible(c *check.C) { + since := daemonTime(c).Unix() + ts := strconv.FormatInt(since, 10) + + out, _ := dockerCmd(c, "run", "--name=foo", "-d", "busybox", "top") + containerID := strings.TrimSpace(out) + c.Assert(waitRun(containerID), checker.IsNil) + + q := url.Values{} + q.Set("since", ts) + + _, body, err := sockRequestRaw("GET", "/events?"+q.Encode(), nil, "") + c.Assert(err, checker.IsNil) + defer body.Close() + + dec := json.NewDecoder(body) + var containerCreateEvent *jsonmessage.JSONMessage + for { + var event jsonmessage.JSONMessage + if err := dec.Decode(&event); err != nil { + if err == io.EOF { + break + } + c.Fatal(err) + } + if event.Status == "create" && event.ID == containerID { + containerCreateEvent = &event + break + } + } + + c.Assert(containerCreateEvent, checker.Not(checker.IsNil)) + c.Assert(containerCreateEvent.Status, checker.Equals, "create") + c.Assert(containerCreateEvent.ID, checker.Equals, containerID) + c.Assert(containerCreateEvent.From, checker.Equals, "busybox") +} diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index b8972844d2..b4dea5f345 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -1874,6 +1874,12 @@ func (s *DockerSuite) TestBuildCancellationKillsSleep(c *check.C) { testRequires(c, DaemonIsLinux) name := "testbuildcancellation" + observer, err := newEventObserver(c) + c.Assert(err, checker.IsNil) + err = observer.Start() + c.Assert(err, checker.IsNil) + defer observer.Stop() + // (Note: one year, will never finish) ctx, err := fakeContext("FROM busybox\nRUN sleep 31536000", nil) if err != nil { @@ -1881,15 +1887,6 @@ func (s *DockerSuite) TestBuildCancellationKillsSleep(c *check.C) { } defer ctx.Close() - eventStart := make(chan struct{}) - eventDie := make(chan struct{}) - - observer, err := newEventObserver(c) - c.Assert(err, checker.IsNil) - err = observer.Start() - c.Assert(err, checker.IsNil) - defer observer.Stop() - buildCmd := exec.Command(dockerBinary, "build", "-t", name, ".") buildCmd.Dir = ctx.Dir @@ -1917,26 +1914,18 @@ func (s *DockerSuite) TestBuildCancellationKillsSleep(c *check.C) { c.Fatalf("Unable to find build container id in build output:\n%s", outputBuffer.String()) } - matchStart := regexp.MustCompile(buildID + `.* start\z`) - matchDie := regexp.MustCompile(buildID + `.* die\z`) - - matcher := func(text string) { - switch { - case matchStart.MatchString(text): - close(eventStart) - case matchDie.MatchString(text): - close(eventDie) - } + testActions := map[string]chan bool{ + "start": make(chan bool), + "die": make(chan bool), } - go observer.Match(matcher) + + go observer.Match(matchEventLine(buildID, "container", testActions)) select { case <-time.After(10 * time.Second): c.Fatal(observer.TimeoutError(buildID, "start")) - case <-eventStart: - // Proceeds from here when we see the container fly past in the - // output of "docker events". - // Now we know the container is running. + case <-testActions["start"]: + // ignore, done } // Send a kill to the `docker build` command. @@ -1952,13 +1941,10 @@ func (s *DockerSuite) TestBuildCancellationKillsSleep(c *check.C) { select { case <-time.After(10 * time.Second): - // If we don't get here in a timely fashion, it wasn't killed. c.Fatal(observer.TimeoutError(buildID, "die")) - case <-eventDie: - // We saw the container shut down in the `docker events` stream, - // as expected. + case <-testActions["die"]: + // ignore, done } - } func (s *DockerSuite) TestBuildRm(c *check.C) { @@ -6489,33 +6475,26 @@ func (s *DockerSuite) TestBuildNoNamedVolume(c *check.C) { func (s *DockerSuite) TestBuildTagEvent(c *check.C) { testRequires(c, DaemonIsLinux) - observer, err := newEventObserver(c, "--filter", "event=tag") - c.Assert(err, check.IsNil) - err = observer.Start() - c.Assert(err, check.IsNil) - defer observer.Stop() + since := daemonTime(c).Unix() dockerFile := `FROM busybox RUN echo events ` - _, err = buildImage("test", dockerFile, false) + _, err := buildImage("test", dockerFile, false) c.Assert(err, check.IsNil) - matchTag := regexp.MustCompile("test:latest") - eventTag := make(chan bool) - matcher := func(text string) { - if matchTag.MatchString(text) { - close(eventTag) + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "type=image") + events := strings.Split(strings.TrimSpace(out), "\n") + actions := eventActionsByIDAndType(c, events, "test:latest", "image") + var foundTag bool + for _, a := range actions { + if a == "tag" { + foundTag = true + break } } - go observer.Match(matcher) - select { - case <-time.After(10 * time.Second): - c.Fatal(observer.TimeoutError("test:latest", "tag")) - case <-eventTag: - // We saw the tag event as expected. - } + c.Assert(foundTag, checker.True, check.Commentf("No tag event found:\n%s", out)) } // #15780 diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go index c9647ec182..82d720455d 100644 --- a/integration-cli/docker_cli_events_test.go +++ b/integration-cli/docker_cli_events_test.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "os/exec" - "regexp" "strconv" "strings" "sync" @@ -67,7 +66,6 @@ func (s *DockerSuite) TestEventsUntag(c *check.C) { } func (s *DockerSuite) TestEventsContainerFailStartDie(c *check.C) { - out, _ := dockerCmd(c, "images", "-q") image := strings.Split(out, "\n")[0] _, _, err := dockerCmdWithError("run", "--name", "testeventdie", image, "blerg") @@ -75,14 +73,12 @@ func (s *DockerSuite) TestEventsContainerFailStartDie(c *check.C) { out, _ = dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) events := strings.Split(out, "\n") - c.Assert(len(events), checker.GreaterThan, 1) //Missing expected event - startEvent := strings.Fields(events[len(events)-3]) - dieEvent := strings.Fields(events[len(events)-2]) - - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(dieEvent[len(dieEvent)-1], checker.Equals, "die", check.Commentf("event should be die, not %#v", dieEvent)) + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 1) //Missing expected event + c.Assert(parseEventAction(c, events[nEvents-3]), checker.Equals, "start") + c.Assert(parseEventAction(c, events[nEvents-2]), checker.Equals, "die") } func (s *DockerSuite) TestEventsLimit(c *check.C) { @@ -114,65 +110,83 @@ func (s *DockerSuite) TestEventsLimit(c *check.C) { func (s *DockerSuite) TestEventsContainerEvents(c *check.C) { testRequires(c, DaemonIsLinux) - dockerCmd(c, "run", "--rm", "busybox", "true") + containerID, _ := dockerCmd(c, "run", "--rm", "--name", "container-events-test", "busybox", "true") + containerID = strings.TrimSpace(containerID) + out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) events := strings.Split(out, "\n") events = events[:len(events)-1] - c.Assert(len(events), checker.GreaterOrEqualThan, 5) //Missing expected event - createEvent := strings.Fields(events[len(events)-5]) - attachEvent := strings.Fields(events[len(events)-4]) - startEvent := strings.Fields(events[len(events)-3]) - dieEvent := strings.Fields(events[len(events)-2]) - destroyEvent := strings.Fields(events[len(events)-1]) - c.Assert(createEvent[len(createEvent)-1], checker.Equals, "create", check.Commentf("event should be create, not %#v", createEvent)) - c.Assert(attachEvent[len(attachEvent)-1], checker.Equals, "attach", check.Commentf("event should be attach, not %#v", attachEvent)) - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(dieEvent[len(dieEvent)-1], checker.Equals, "die", check.Commentf("event should be die, not %#v", dieEvent)) - c.Assert(destroyEvent[len(destroyEvent)-1], checker.Equals, "destroy", check.Commentf("event should be destroy, not %#v", destroyEvent)) + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + containerEvents := eventActionsByIDAndType(c, events, "container-events-test", "container") + c.Assert(containerEvents, checker.HasLen, 5, check.Commentf("events: %v", events)) + + c.Assert(containerEvents[0], checker.Equals, "create", check.Commentf(out)) + c.Assert(containerEvents[1], checker.Equals, "attach", check.Commentf(out)) + c.Assert(containerEvents[2], checker.Equals, "start", check.Commentf(out)) + c.Assert(containerEvents[3], checker.Equals, "die", check.Commentf(out)) + c.Assert(containerEvents[4], checker.Equals, "destroy", check.Commentf(out)) } func (s *DockerSuite) TestEventsContainerEventsSinceUnixEpoch(c *check.C) { testRequires(c, DaemonIsLinux) - dockerCmd(c, "run", "--rm", "busybox", "true") + dockerCmd(c, "run", "--rm", "--name", "since-epoch-test", "busybox", "true") timeBeginning := time.Unix(0, 0).Format(time.RFC3339Nano) timeBeginning = strings.Replace(timeBeginning, "Z", ".000000000Z", -1) - out, _ := dockerCmd(c, "events", fmt.Sprintf("--since='%s'", timeBeginning), - fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since='%s'", timeBeginning), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) events := strings.Split(out, "\n") events = events[:len(events)-1] - c.Assert(len(events), checker.GreaterOrEqualThan, 5) //Missing expected event - createEvent := strings.Fields(events[len(events)-5]) - attachEvent := strings.Fields(events[len(events)-4]) - startEvent := strings.Fields(events[len(events)-3]) - dieEvent := strings.Fields(events[len(events)-2]) - destroyEvent := strings.Fields(events[len(events)-1]) - c.Assert(createEvent[len(createEvent)-1], checker.Equals, "create", check.Commentf("event should be create, not %#v", createEvent)) - c.Assert(attachEvent[len(attachEvent)-1], checker.Equals, "attach", check.Commentf("event should be attach, not %#v", attachEvent)) - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(dieEvent[len(dieEvent)-1], checker.Equals, "die", check.Commentf("event should be die, not %#v", dieEvent)) - c.Assert(destroyEvent[len(destroyEvent)-1], checker.Equals, "destroy", check.Commentf("event should be destroy, not %#v", destroyEvent)) + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + containerEvents := eventActionsByIDAndType(c, events, "since-epoch-test", "container") + c.Assert(containerEvents, checker.HasLen, 5, check.Commentf("events: %v", events)) + + c.Assert(containerEvents[0], checker.Equals, "create", check.Commentf(out)) + c.Assert(containerEvents[1], checker.Equals, "attach", check.Commentf(out)) + c.Assert(containerEvents[2], checker.Equals, "start", check.Commentf(out)) + c.Assert(containerEvents[3], checker.Equals, "die", check.Commentf(out)) + c.Assert(containerEvents[4], checker.Equals, "destroy", check.Commentf(out)) } func (s *DockerSuite) TestEventsImageUntagDelete(c *check.C) { testRequires(c, DaemonIsLinux) + + observer, err := newEventObserver(c) + c.Assert(err, checker.IsNil) + err = observer.Start() + c.Assert(err, checker.IsNil) + defer observer.Stop() + name := "testimageevents" - _, err := buildImage(name, + imageID, err := buildImage(name, `FROM scratch MAINTAINER "docker"`, true) c.Assert(err, checker.IsNil) c.Assert(deleteImages(name), checker.IsNil) - out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) - events := strings.Split(out, "\n") - events = events[:len(events)-1] - c.Assert(len(events), checker.GreaterOrEqualThan, 2) //Missing expected event - untagEvent := strings.Fields(events[len(events)-2]) - deleteEvent := strings.Fields(events[len(events)-1]) - c.Assert(untagEvent[len(untagEvent)-1], checker.Equals, "untag", check.Commentf("untag should be untag, not %#v", untagEvent)) - c.Assert(deleteEvent[len(deleteEvent)-1], checker.Equals, "delete", check.Commentf("untag should be delete, not %#v", untagEvent)) + testActions := map[string]chan bool{ + "untag": make(chan bool), + "delete": make(chan bool), + } + + go observer.Match(matchEventLine(imageID, "image", testActions)) + + select { + case <-time.After(10 * time.Second): + c.Fatal(observer.TimeoutError(imageID, "untag")) + case <-testActions["untag"]: + // ignore, done + } + + select { + case <-time.After(10 * time.Second): + c.Fatal(observer.TimeoutError(imageID, "delete")) + case <-testActions["delete"]: + // ignore, done + } } func (s *DockerSuite) TestEventsImageTag(c *check.C) { @@ -189,10 +203,11 @@ func (s *DockerSuite) TestEventsImageTag(c *check.C) { events := strings.Split(strings.TrimSpace(out), "\n") c.Assert(events, checker.HasLen, 1, check.Commentf("was expecting 1 event. out=%s", out)) event := strings.TrimSpace(events[0]) - expectedStr := image + ": tag" - - c.Assert(event, checker.HasSuffix, expectedStr, check.Commentf("wrong event format. expected='%s' got=%s", expectedStr, event)) + matches := parseEventText(event) + c.Assert(matches, checker.Not(checker.IsNil)) + c.Assert(matchEventID(matches, image), checker.True, check.Commentf("matches: %v\nout:\n%s", matches, out)) + c.Assert(matches["action"], checker.Equals, "tag") } func (s *DockerSuite) TestEventsImagePull(c *check.C) { @@ -208,68 +223,46 @@ func (s *DockerSuite) TestEventsImagePull(c *check.C) { events := strings.Split(strings.TrimSpace(out), "\n") event := strings.TrimSpace(events[len(events)-1]) - - c.Assert(event, checker.HasSuffix, "hello-world:latest: pull", check.Commentf("Missing pull event - got:%q", event)) + matches := parseEventText(event) + c.Assert(matches, checker.Not(checker.IsNil)) + c.Assert(matches["id"], checker.Equals, "hello-world:latest") + c.Assert(matches["action"], checker.Equals, "pull") } func (s *DockerSuite) TestEventsImageImport(c *check.C) { testRequires(c, DaemonIsLinux) - observer, err := newEventObserver(c) - c.Assert(err, checker.IsNil) - - err = observer.Start() - c.Assert(err, checker.IsNil) - defer observer.Stop() - out, _ := dockerCmd(c, "run", "-d", "busybox", "true") cleanedContainerID := strings.TrimSpace(out) - out, _, err = runCommandPipelineWithOutput( + since := daemonTime(c).Unix() + out, _, err := runCommandPipelineWithOutput( exec.Command(dockerBinary, "export", cleanedContainerID), exec.Command(dockerBinary, "import", "-"), ) c.Assert(err, checker.IsNil, check.Commentf("import failed with output: %q", out)) imageRef := strings.TrimSpace(out) - eventImport := make(chan bool) - matchImport := regexp.MustCompile(imageRef + `: import\z`) - matcher := func(text string) { - if matchImport.MatchString(text) { - close(eventImport) - } - } - go observer.Match(matcher) - - select { - case <-time.After(5 * time.Second): - c.Fatal(observer.TimeoutError(imageRef, "import")) - case <-eventImport: - // ignore, done - } + out, _ = dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=import") + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(events, checker.HasLen, 1) + matches := parseEventText(events[0]) + c.Assert(matches["id"], checker.Equals, imageRef, check.Commentf("matches: %v\nout:\n%s\n", matches, out)) + c.Assert(matches["action"], checker.Equals, "import", check.Commentf("matches: %v\nout:\n%s\n", matches, out)) } func (s *DockerSuite) TestEventsFilters(c *check.C) { testRequires(c, DaemonIsLinux) - parseEvents := func(out, match string) { - events := strings.Split(out, "\n") - events = events[:len(events)-1] - for _, event := range events { - eventFields := strings.Fields(event) - eventName := eventFields[len(eventFields)-1] - c.Assert(eventName, checker.Matches, match) - } - } since := daemonTime(c).Unix() dockerCmd(c, "run", "--rm", "busybox", "true") dockerCmd(c, "run", "--rm", "busybox", "true") out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=die") - parseEvents(out, "die") + parseEvents(c, out, "die") out, _ = dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=die", "--filter", "event=start") - parseEvents(out, "((die)|(start))") + parseEvents(c, out, "die|start") // make sure we at least got 2 start events count := strings.Count(out, "start") @@ -385,15 +378,9 @@ func (s *DockerSuite) TestEventsFilterContainer(c *check.C) { return fmt.Errorf("expected 4 events, got %v", events) } for _, event := range events { - e := strings.Fields(event) - if len(e) < 3 { - return fmt.Errorf("got malformed event: %s", event) - } - - // Check the id - parsedID := strings.TrimSuffix(e[1], ":") - if parsedID != id { - return fmt.Errorf("expected event for container id %s: %s - parsed container id: %s", id, event, parsedID) + matches := parseEventText(event) + if !matchEventID(matches, id) { + return fmt.Errorf("expected event for container id %s: %s - parsed container id: %s", id, event, matches["id"]) } } return nil @@ -450,21 +437,21 @@ func (s *DockerSuite) TestEventsStreaming(c *check.C) { select { case <-time.After(5 * time.Second): c.Fatal(observer.TimeoutError(containerID, "create")) - case <-eventCreate: + case <-testActions["create"]: // ignore, done } select { case <-time.After(5 * time.Second): c.Fatal(observer.TimeoutError(containerID, "start")) - case <-eventStart: + case <-testActions["start"]: // ignore, done } select { case <-time.After(5 * time.Second): c.Fatal(observer.TimeoutError(containerID, "die")) - case <-eventDie: + case <-testActions["die"]: // ignore, done } @@ -490,7 +477,7 @@ func (s *DockerSuite) TestEventsCommit(c *check.C) { dockerCmd(c, "stop", cID) out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " commit\n", check.Commentf("Missing 'commit' log event")) + c.Assert(out, checker.Contains, "commit", check.Commentf("Missing 'commit' log event")) } func (s *DockerSuite) TestEventsCopy(c *check.C) { @@ -515,12 +502,12 @@ func (s *DockerSuite) TestEventsCopy(c *check.C) { dockerCmd(c, "cp", "cptest:/tmp/file", tempFile.Name()) out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " archive-path\n", check.Commentf("Missing 'archive-path' log event\n")) + c.Assert(out, checker.Contains, "archive-path", check.Commentf("Missing 'archive-path' log event\n")) dockerCmd(c, "cp", tempFile.Name(), "cptest:/tmp/filecopy") out, _ = dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " extract-to-dir\n", check.Commentf("Missing 'extract-to-dir' log event")) + c.Assert(out, checker.Contains, "extract-to-dir", check.Commentf("Missing 'extract-to-dir' log event")) } func (s *DockerSuite) TestEventsResize(c *check.C) { @@ -539,7 +526,7 @@ func (s *DockerSuite) TestEventsResize(c *check.C) { dockerCmd(c, "stop", cID) out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " resize\n", check.Commentf("Missing 'resize' log event")) + c.Assert(out, checker.Contains, "resize", check.Commentf("Missing 'resize' log event")) } func (s *DockerSuite) TestEventsAttach(c *check.C) { @@ -571,7 +558,7 @@ func (s *DockerSuite) TestEventsAttach(c *check.C) { dockerCmd(c, "stop", cID) out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " attach\n", check.Commentf("Missing 'attach' log event")) + c.Assert(out, checker.Contains, "attach", check.Commentf("Missing 'attach' log event")) } func (s *DockerSuite) TestEventsRename(c *check.C) { @@ -582,7 +569,7 @@ func (s *DockerSuite) TestEventsRename(c *check.C) { dockerCmd(c, "rename", "oldName", "newName") out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=newName", "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " rename\n", check.Commentf("Missing 'rename' log event\n")) + c.Assert(out, checker.Contains, "rename", check.Commentf("Missing 'rename' log event\n")) } func (s *DockerSuite) TestEventsTop(c *check.C) { @@ -597,7 +584,7 @@ func (s *DockerSuite) TestEventsTop(c *check.C) { dockerCmd(c, "stop", cID) out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " top\n", check.Commentf("Missing 'top' log event")) + c.Assert(out, checker.Contains, " top", check.Commentf("Missing 'top' log event")) } // #13753 @@ -624,5 +611,5 @@ func (s *DockerRegistrySuite) TestEventsImageFilterPush(c *check.C) { dockerCmd(c, "push", repoName) out, _ = dockerCmd(c, "events", "--since=0", "-f", "image="+repoName, "-f", "event=push", "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, repoName+": push\n", check.Commentf("Missing 'push' log event")) + c.Assert(out, checker.Contains, repoName, check.Commentf("Missing 'push' log event for %s", repoName)) } diff --git a/integration-cli/docker_cli_events_unix_test.go b/integration-cli/docker_cli_events_unix_test.go index 7935c50392..b287a65e1b 100644 --- a/integration-cli/docker_cli_events_unix_test.go +++ b/integration-cli/docker_cli_events_unix_test.go @@ -65,18 +65,14 @@ func (s *DockerSuite) TestEventsOOMDisableFalse(c *check.C) { out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=oomFalse", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) events := strings.Split(strings.TrimSuffix(out, "\n"), "\n") - c.Assert(len(events), checker.GreaterOrEqualThan, 5) //Missing expected event + nEvents := len(events) - createEvent := strings.Fields(events[len(events)-5]) - attachEvent := strings.Fields(events[len(events)-4]) - startEvent := strings.Fields(events[len(events)-3]) - oomEvent := strings.Fields(events[len(events)-2]) - dieEvent := strings.Fields(events[len(events)-1]) - c.Assert(createEvent[len(createEvent)-1], checker.Equals, "create", check.Commentf("event should be create, not %#v", createEvent)) - c.Assert(attachEvent[len(attachEvent)-1], checker.Equals, "attach", check.Commentf("event should be attach, not %#v", attachEvent)) - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(oomEvent[len(oomEvent)-1], checker.Equals, "oom", check.Commentf("event should be oom, not %#v", oomEvent)) - c.Assert(dieEvent[len(dieEvent)-1], checker.Equals, "die", check.Commentf("event should be die, not %#v", dieEvent)) + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + c.Assert(parseEventAction(c, events[nEvents-5]), checker.Equals, "create") + c.Assert(parseEventAction(c, events[nEvents-4]), checker.Equals, "attach") + c.Assert(parseEventAction(c, events[nEvents-3]), checker.Equals, "start") + c.Assert(parseEventAction(c, events[nEvents-2]), checker.Equals, "oom") + c.Assert(parseEventAction(c, events[nEvents-1]), checker.Equals, "die") } func (s *DockerSuite) TestEventsOOMDisableTrue(c *check.C) { @@ -98,17 +94,13 @@ func (s *DockerSuite) TestEventsOOMDisableTrue(c *check.C) { out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=oomTrue", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) events := strings.Split(strings.TrimSuffix(out, "\n"), "\n") - c.Assert(len(events), checker.GreaterOrEqualThan, 4) //Missing expected event + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 4) //Missing expected event - createEvent := strings.Fields(events[len(events)-4]) - attachEvent := strings.Fields(events[len(events)-3]) - startEvent := strings.Fields(events[len(events)-2]) - oomEvent := strings.Fields(events[len(events)-1]) - - c.Assert(createEvent[len(createEvent)-1], checker.Equals, "create", check.Commentf("event should be create, not %#v", createEvent)) - c.Assert(attachEvent[len(attachEvent)-1], checker.Equals, "attach", check.Commentf("event should be attach, not %#v", attachEvent)) - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(oomEvent[len(oomEvent)-1], checker.Equals, "oom", check.Commentf("event should be oom, not %#v", oomEvent)) + c.Assert(parseEventAction(c, events[nEvents-4]), checker.Equals, "create") + c.Assert(parseEventAction(c, events[nEvents-3]), checker.Equals, "attach") + c.Assert(parseEventAction(c, events[nEvents-2]), checker.Equals, "start") + c.Assert(parseEventAction(c, events[nEvents-1]), checker.Equals, "oom") out, _ = dockerCmd(c, "inspect", "-f", "{{.State.Status}}", "oomTrue") c.Assert(strings.TrimSpace(out), checker.Equals, "running", check.Commentf("container should be still running")) @@ -116,17 +108,17 @@ func (s *DockerSuite) TestEventsOOMDisableTrue(c *check.C) { } // #18453 -func (s *DockerSuite) TestEventsContainerFilter(c *check.C) { +func (s *DockerSuite) TestEventsContainerFilterByName(c *check.C) { testRequires(c, DaemonIsLinux) - out, _ := dockerCmd(c, "run", "--name=foo", "-d", "busybox", "top") - c1 := strings.TrimSpace(out) - waitRun(c1) - out, _ = dockerCmd(c, "run", "--name=bar", "-d", "busybox", "top") - c2 := strings.TrimSpace(out) - waitRun(c2) - out, _ = dockerCmd(c, "events", "-f", "container=foo", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) - c.Assert(out, checker.Contains, c1, check.Commentf("Missing event of container (foo)")) - c.Assert(out, checker.Not(checker.Contains), c2, check.Commentf("Should not contain event of container (bar)")) + cOut, _ := dockerCmd(c, "run", "--name=foo", "-d", "busybox", "top") + c1 := strings.TrimSpace(cOut) + waitRun("foo") + cOut, _ = dockerCmd(c, "run", "--name=bar", "-d", "busybox", "top") + c2 := strings.TrimSpace(cOut) + waitRun("bar") + out, _ := dockerCmd(c, "events", "-f", "container=foo", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + c.Assert(out, checker.Contains, c1, check.Commentf(out)) + c.Assert(out, checker.Not(checker.Contains), c2, check.Commentf(out)) } // #18453 diff --git a/integration-cli/docker_cli_pause_test.go b/integration-cli/docker_cli_pause_test.go index afe62c60db..a88d32a0e1 100644 --- a/integration-cli/docker_cli_pause_test.go +++ b/integration-cli/docker_cli_pause_test.go @@ -23,15 +23,11 @@ func (s *DockerSuite) TestPause(c *check.C) { dockerCmd(c, "unpause", name) out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) - events := strings.Split(out, "\n") - c.Assert(len(events) > 1, checker.Equals, true) - - pauseEvent := strings.Fields(events[len(events)-3]) - unpauseEvent := strings.Fields(events[len(events)-2]) - - c.Assert(pauseEvent[len(pauseEvent)-1], checker.Equals, "pause") - c.Assert(unpauseEvent[len(unpauseEvent)-1], checker.Equals, "unpause") + events := strings.Split(strings.TrimSpace(out), "\n") + actions := eventActionsByIDAndType(c, events, name, "container") + c.Assert(actions[len(actions)-2], checker.Equals, "pause") + c.Assert(actions[len(actions)-1], checker.Equals, "unpause") } func (s *DockerSuite) TestPauseMultipleContainers(c *check.C) { @@ -53,21 +49,12 @@ func (s *DockerSuite) TestPauseMultipleContainers(c *check.C) { dockerCmd(c, append([]string{"unpause"}, containers...)...) out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) - events := strings.Split(out, "\n") - c.Assert(len(events) > len(containers)*3-2, checker.Equals, true) + events := strings.Split(strings.TrimSpace(out), "\n") - pauseEvents := make([][]string, len(containers)) - unpauseEvents := make([][]string, len(containers)) - for i := range containers { - pauseEvents[i] = strings.Fields(events[len(events)-len(containers)*2-1+i]) - unpauseEvents[i] = strings.Fields(events[len(events)-len(containers)-1+i]) - } + for _, name := range containers { + actions := eventActionsByIDAndType(c, events, name, "container") - for _, pauseEvent := range pauseEvents { - c.Assert(pauseEvent[len(pauseEvent)-1], checker.Equals, "pause") + c.Assert(actions[len(actions)-2], checker.Equals, "pause") + c.Assert(actions[len(actions)-1], checker.Equals, "unpause") } - for _, unpauseEvent := range unpauseEvents { - c.Assert(unpauseEvent[len(unpauseEvent)-1], checker.Equals, "unpause") - } - } diff --git a/integration-cli/events_utils.go b/integration-cli/events_utils.go index faff4fc6f8..59e88477d1 100644 --- a/integration-cli/events_utils.go +++ b/integration-cli/events_utils.go @@ -7,10 +7,24 @@ import ( "io" "os/exec" "strconv" + "strings" + "github.com/docker/docker/pkg/integration/checker" "github.com/go-check/check" ) +var ( + reTimestamp = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{9}(:?(:?(:?-|\+)\d{2}:\d{2})|Z)` + reEventType = `(?P\w+)` + reAction = `(?P\w+)` + reID = `(?P[^\s]+)` + reAttributes = `(\s\((?P[^\)]+)\))?` + reString = fmt.Sprintf(`\A%s\s%s\s%s\s%s%s\z`, reTimestamp, reEventType, reAction, reID, reAttributes) + + // eventCliRegexp is a regular expression that matches all possible event outputs in the cli + eventCliRegexp = regexp.MustCompile(reString) +) + // eventMatcher is a function that tries to match an event input. type eventMatcher func(text string) @@ -25,7 +39,11 @@ type eventObserver struct { // without running it. Users must call `eventObserver.Start` to start the command. func newEventObserver(c *check.C, 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 *check.C, since int64, args ...string) (*eventObserver, error) { cmdArgs := []string{"events", "--since", strconv.FormatInt(since, 10)} if len(args) > 0 { cmdArgs = append(cmdArgs, args...) @@ -51,6 +69,7 @@ func (e *eventObserver) Start() error { // Stop stops the events command. func (e *eventObserver) Stop() { e.command.Process.Kill() + e.command.Process.Release() } // Match tries to match the events output with a given matcher. @@ -76,3 +95,103 @@ func (e *eventObserver) TimeoutError(id, event string) error { func (e *eventObserver) output() string { return e.buffer.String() } + +// matchEventLine matches a text with the event regular expression. +// It returns the action and true if the regular expression matches with the given id and event type. +// It returns an empty string and false if there is no match. +func matchEventLine(id, eventType string, actions map[string]chan bool) eventMatcher { + return func(text string) { + matches := parseEventText(text) + if matches == nil { + return + } + + if matchIDAndEventType(matches, id, eventType) { + if ch, ok := actions[matches["action"]]; ok { + close(ch) + } + } + } +} + +// parseEventText parses a line of events coming from the cli and returns +// the matchers in a map. +func parseEventText(text string) map[string]string { + matches := eventCliRegexp.FindAllStringSubmatch(text, -1) + if len(matches) == 0 { + return nil + } + + names := eventCliRegexp.SubexpNames() + md := map[string]string{} + for i, n := range matches[0] { + md[names[i]] = n + } + return md +} + +// parseEventAction parses an event text and returns the action. +// It fails if the text is not in the event format. +func parseEventAction(c *check.C, text string) string { + matches := parseEventText(text) + c.Assert(matches, checker.Not(checker.IsNil)) + 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 *check.C, events []string, id, eventType string) []string { + var filtered []string + for _, event := range events { + matches := parseEventText(event) + c.Assert(matches, checker.Not(checker.IsNil)) + 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 *check.C, out, match string) { + events := strings.Split(strings.TrimSpace(out), "\n") + for _, event := range events { + matches := parseEventText(event) + c.Assert(matches, checker.Not(checker.IsNil)) + matched, err := regexp.MatchString(match, matches["action"]) + c.Assert(err, checker.IsNil) + c.Assert(matched, checker.True) + } +} + +func parseEventsWithID(c *check.C, out, match, id string) { + events := strings.Split(strings.TrimSpace(out), "\n") + for _, event := range events { + matches := parseEventText(event) + c.Assert(matches, checker.Not(checker.IsNil)) + c.Assert(matchEventID(matches, id), checker.True) + + matched, err := regexp.MatchString(match, matches["action"]) + c.Assert(err, checker.IsNil) + c.Assert(matched, checker.True) + } +}