فهرست منبع

Merge pull request #18888 from calavera/event_types

Event all the things!
David Calavera 9 سال پیش
والد
کامیت
723be0a332
40فایلهای تغییر یافته به همراه1318 افزوده شده و 565 حذف شده
  1. 60 2
      api/client/events.go
  2. 14 9
      api/client/stats.go
  3. 1 0
      api/server/router/network/backend.go
  4. 1 15
      api/server/router/network/network_routes.go
  5. 2 2
      api/server/router/system/backend.go
  6. 3 2
      api/server/router/system/system_routes.go
  7. 38 0
      api/types/events/events.go
  8. 16 0
      api/types/filters/parse.go
  9. 18 0
      api/types/filters/parse_test.go
  10. 7 1
      container/container_unix.go
  11. 1 1
      container/container_windows.go
  12. 6 6
      daemon/archive.go
  13. 22 5
      daemon/container_operations_unix.go
  14. 5 0
      daemon/create.go
  15. 25 53
      daemon/daemon.go
  16. 1 0
      daemon/delete.go
  17. 72 5
      daemon/events.go
  18. 30 12
      daemon/events/events.go
  19. 26 9
      daemon/events/events_test.go
  20. 49 19
      daemon/events/filter.go
  21. 36 0
      daemon/events_test.go
  22. 4 4
      daemon/image_delete.go
  23. 1 1
      daemon/import.go
  24. 27 1
      daemon/network.go
  25. 1 1
      daemon/start.go
  26. 2 0
      daemon/update.go
  27. 11 0
      daemon/volumes_unix.go
  28. 3 4
      distribution/pull.go
  29. 3 4
      distribution/push.go
  30. 6 0
      docs/misc/deprecated.md
  31. 56 8
      docs/reference/api/docker_remote_api_v1.22.md
  32. 63 42
      docs/reference/commandline/events.md
  33. 9 0
      errors/daemon.go
  34. 43 0
      integration-cli/docker_api_events_test.go
  35. 11 117
      integration-cli/docker_cli_build_test.go
  36. 88 0
      integration-cli/docker_cli_build_unix_test.go
  37. 138 179
      integration-cli/docker_cli_events_test.go
  38. 251 22
      integration-cli/docker_cli_events_unix_test.go
  39. 9 22
      integration-cli/docker_cli_pause_test.go
  40. 159 19
      integration-cli/events_utils.go

+ 60 - 2
api/client/events.go

@@ -1,11 +1,18 @@
 package client
 package client
 
 
 import (
 import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"strings"
+	"time"
+
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
+	eventtypes "github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/filters"
 	Cli "github.com/docker/docker/cli"
 	Cli "github.com/docker/docker/cli"
 	"github.com/docker/docker/opts"
 	"github.com/docker/docker/opts"
-	"github.com/docker/docker/pkg/jsonmessage"
+	"github.com/docker/docker/pkg/jsonlog"
 	flag "github.com/docker/docker/pkg/mflag"
 	flag "github.com/docker/docker/pkg/mflag"
 )
 )
 
 
@@ -46,5 +53,56 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
 	}
 	}
 	defer responseBody.Close()
 	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 {
+	return decodeEvents(input, func(event eventtypes.Message, err error) error {
+		if err != nil {
+			return err
+		}
+		printOutput(event, output)
+		return nil
+	})
+}
+
+type eventProcessor func(event eventtypes.Message, err error) error
+
+func decodeEvents(input io.Reader, ep eventProcessor) error {
+	dec := json.NewDecoder(input)
+	for {
+		var event eventtypes.Message
+		err := dec.Decode(&event)
+		if err != nil && err == io.EOF {
+			break
+		}
+
+		if procErr := ep(event, err); procErr != nil {
+			return procErr
+		}
+	}
+	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")
 }
 }

+ 14 - 9
api/client/stats.go

@@ -11,8 +11,9 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/events"
+	"github.com/docker/docker/api/types/filters"
 	Cli "github.com/docker/docker/cli"
 	Cli "github.com/docker/docker/cli"
-	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/docker/go-units"
 	"github.com/docker/go-units"
 )
 )
 
 
@@ -189,7 +190,11 @@ func (cli *DockerCli) CmdStats(args ...string) error {
 			err   error
 			err   error
 		}
 		}
 		getNewContainers := func(c chan<- watch) {
 		getNewContainers := func(c chan<- watch) {
-			options := types.EventsOptions{}
+			f := filters.NewArgs()
+			f.Add("type", "container")
+			options := types.EventsOptions{
+				Filters: f,
+			}
 			resBody, err := cli.client.Events(options)
 			resBody, err := cli.client.Events(options)
 			if err != nil {
 			if err != nil {
 				c <- watch{err: err}
 				c <- watch{err: err}
@@ -197,15 +202,15 @@ func (cli *DockerCli) CmdStats(args ...string) error {
 			}
 			}
 			defer resBody.Close()
 			defer resBody.Close()
 
 
-			dec := json.NewDecoder(resBody)
-			for {
-				var j *jsonmessage.JSONMessage
-				if err := dec.Decode(&j); err != nil {
+			decodeEvents(resBody, func(event events.Message, err error) error {
+				if err != nil {
 					c <- watch{err: err}
 					c <- watch{err: err}
-					return
+					return nil
 				}
 				}
-				c <- watch{j.ID[:12], j.Status, nil}
-			}
+
+				c <- watch{event.ID[:12], event.Action, nil}
+				return nil
+			})
 		}
 		}
 		go func(stopChan chan<- error) {
 		go func(stopChan chan<- error) {
 			cChan := make(chan watch)
 			cChan := make(chan watch)

+ 1 - 0
api/server/router/network/backend.go

@@ -19,4 +19,5 @@ type Backend interface {
 	DisconnectContainerFromNetwork(containerName string,
 	DisconnectContainerFromNetwork(containerName string,
 		network libnetwork.Network) error
 		network libnetwork.Network) error
 	NetworkControllerEnabled() bool
 	NetworkControllerEnabled() bool
+	DeleteNetwork(name string) error
 }
 }

+ 1 - 15
api/server/router/network/network_routes.go

@@ -148,21 +148,7 @@ func (n *networkRouter) postNetworkDisconnect(ctx context.Context, w http.Respon
 }
 }
 
 
 func (n *networkRouter) deleteNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 func (n *networkRouter) deleteNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
-	if err := httputils.ParseForm(r); err != nil {
-		return err
-	}
-
-	nw, err := n.backend.FindNetwork(vars["id"])
-	if err != nil {
-		return err
-	}
-
-	if runconfig.IsPreDefinedNetwork(nw.Name()) {
-		return httputils.WriteJSON(w, http.StatusForbidden,
-			fmt.Sprintf("%s is a pre-defined network and cannot be removed", nw.Name()))
-	}
-
-	return nw.Delete()
+	return n.backend.DeleteNetwork(vars["id"])
 }
 }
 
 
 func buildNetworkResource(nw libnetwork.Network) *types.NetworkResource {
 func buildNetworkResource(nw libnetwork.Network) *types.NetworkResource {

+ 2 - 2
api/server/router/system/backend.go

@@ -2,8 +2,8 @@ package system
 
 
 import (
 import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/filters"
-	"github.com/docker/docker/pkg/jsonmessage"
 )
 )
 
 
 // Backend is the methods that need to be implemented to provide
 // Backend is the methods that need to be implemented to provide
@@ -11,7 +11,7 @@ import (
 type Backend interface {
 type Backend interface {
 	SystemInfo() (*types.Info, error)
 	SystemInfo() (*types.Info, error)
 	SystemVersion() types.Version
 	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{})
 	UnsubscribeFromEvents(chan interface{})
 	AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error)
 	AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error)
 }
 }

+ 3 - 2
api/server/router/system/system_routes.go

@@ -9,10 +9,10 @@ import (
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api/server/httputils"
 	"github.com/docker/docker/api/server/httputils"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/filters"
 	timetypes "github.com/docker/docker/api/types/time"
 	timetypes "github.com/docker/docker/api/types/time"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/ioutils"
-	"github.com/docker/docker/pkg/jsonmessage"
 	"golang.org/x/net/context"
 	"golang.org/x/net/context"
 )
 )
 
 
@@ -98,8 +98,9 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *
 	for {
 	for {
 		select {
 		select {
 		case ev := <-l:
 		case ev := <-l:
-			jev, ok := ev.(*jsonmessage.JSONMessage)
+			jev, ok := ev.(events.Message)
 			if !ok {
 			if !ok {
+				logrus.Warnf("unexpected event message: %q", ev)
 				continue
 				continue
 			}
 			}
 			if err := enc.Encode(jev); err != nil {
 			if err := enc.Encode(jev); err != nil {

+ 38 - 0
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"`
+}

+ 16 - 0
api/types/filters/parse.go

@@ -197,6 +197,22 @@ func (filters Args) ExactMatch(field, source string) bool {
 	return false
 	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.
 // Include returns true if the name of the field to filter is in the filters.
 func (filters Args) Include(field string) bool {
 func (filters Args) Include(field string) bool {
 	_, ok := filters.fields[field]
 	_, ok := filters.fields[field]

+ 18 - 0
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)
 		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)
+		}
+	}
+}

+ 7 - 1
container/container_unix.go

@@ -618,7 +618,7 @@ func detachMounted(path string) error {
 }
 }
 
 
 // UnmountVolumes unmounts all volumes
 // UnmountVolumes unmounts all volumes
-func (container *Container) UnmountVolumes(forceSyscall bool) error {
+func (container *Container) UnmountVolumes(forceSyscall bool, volumeEventLog func(name, action string, attributes map[string]string)) error {
 	var (
 	var (
 		volumeMounts []volume.MountPoint
 		volumeMounts []volume.MountPoint
 		err          error
 		err          error
@@ -649,6 +649,12 @@ func (container *Container) UnmountVolumes(forceSyscall bool) error {
 			if err := volumeMount.Volume.Unmount(); err != nil {
 			if err := volumeMount.Volume.Unmount(); err != nil {
 				return err
 				return err
 			}
 			}
+
+			attributes := map[string]string{
+				"driver":    volumeMount.Volume.DriverName(),
+				"container": container.ID,
+			}
+			volumeEventLog(volumeMount.Volume.Name(), "unmount", attributes)
 		}
 		}
 	}
 	}
 
 

+ 1 - 1
container/container_windows.go

@@ -39,7 +39,7 @@ func (container *Container) IpcMounts() []execdriver.Mount {
 }
 }
 
 
 // UnmountVolumes explicitly unmounts volumes from the container.
 // UnmountVolumes explicitly unmounts volumes from the container.
-func (container *Container) UnmountVolumes(forceSyscall bool) error {
+func (container *Container) UnmountVolumes(forceSyscall bool, volumeEventLog func(name, action string, attributes map[string]string)) error {
 	return nil
 	return nil
 }
 }
 
 

+ 6 - 6
daemon/archive.go

@@ -84,7 +84,7 @@ func (daemon *Daemon) containerStatPath(container *container.Container, path str
 	defer daemon.Unmount(container)
 	defer daemon.Unmount(container)
 
 
 	err = daemon.mountVolumes(container)
 	err = daemon.mountVolumes(container)
-	defer container.UnmountVolumes(true)
+	defer container.UnmountVolumes(true, daemon.LogVolumeEvent)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -119,7 +119,7 @@ func (daemon *Daemon) containerArchivePath(container *container.Container, path
 	defer func() {
 	defer func() {
 		if err != nil {
 		if err != nil {
 			// unmount any volumes
 			// unmount any volumes
-			container.UnmountVolumes(true)
+			container.UnmountVolumes(true, daemon.LogVolumeEvent)
 			// unmount the container's rootfs
 			// unmount the container's rootfs
 			daemon.Unmount(container)
 			daemon.Unmount(container)
 		}
 		}
@@ -154,7 +154,7 @@ func (daemon *Daemon) containerArchivePath(container *container.Container, path
 
 
 	content = ioutils.NewReadCloserWrapper(data, func() error {
 	content = ioutils.NewReadCloserWrapper(data, func() error {
 		err := data.Close()
 		err := data.Close()
-		container.UnmountVolumes(true)
+		container.UnmountVolumes(true, daemon.LogVolumeEvent)
 		daemon.Unmount(container)
 		daemon.Unmount(container)
 		container.Unlock()
 		container.Unlock()
 		return err
 		return err
@@ -181,7 +181,7 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path
 	defer daemon.Unmount(container)
 	defer daemon.Unmount(container)
 
 
 	err = daemon.mountVolumes(container)
 	err = daemon.mountVolumes(container)
-	defer container.UnmountVolumes(true)
+	defer container.UnmountVolumes(true, daemon.LogVolumeEvent)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -283,7 +283,7 @@ func (daemon *Daemon) containerCopy(container *container.Container, resource str
 	defer func() {
 	defer func() {
 		if err != nil {
 		if err != nil {
 			// unmount any volumes
 			// unmount any volumes
-			container.UnmountVolumes(true)
+			container.UnmountVolumes(true, daemon.LogVolumeEvent)
 			// unmount the container's rootfs
 			// unmount the container's rootfs
 			daemon.Unmount(container)
 			daemon.Unmount(container)
 		}
 		}
@@ -320,7 +320,7 @@ func (daemon *Daemon) containerCopy(container *container.Container, resource str
 
 
 	reader := ioutils.NewReadCloserWrapper(archive, func() error {
 	reader := ioutils.NewReadCloserWrapper(archive, func() error {
 		err := archive.Close()
 		err := archive.Close()
-		container.UnmountVolumes(true)
+		container.UnmountVolumes(true, daemon.LogVolumeEvent)
 		daemon.Unmount(container)
 		daemon.Unmount(container)
 		container.Unlock()
 		container.Unlock()
 		return err
 		return err

+ 22 - 5
daemon/container_operations_unix.go

@@ -699,6 +699,7 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName
 		return derr.ErrorCodeJoinInfo.WithArgs(err)
 		return derr.ErrorCodeJoinInfo.WithArgs(err)
 	}
 	}
 
 
+	daemon.LogNetworkEventWithAttributes(n, "connect", map[string]string{"container": container.ID})
 	return nil
 	return nil
 }
 }
 
 
@@ -719,6 +720,11 @@ func (daemon *Daemon) DisconnectFromNetwork(container *container.Container, n li
 	if err := container.ToDiskLocking(); err != nil {
 	if err := container.ToDiskLocking(); err != nil {
 		return fmt.Errorf("Error saving container to disk: %v", err)
 		return fmt.Errorf("Error saving container to disk: %v", err)
 	}
 	}
+
+	attributes := map[string]string{
+		"container": container.ID,
+	}
+	daemon.LogNetworkEventWithAttributes(n, "disconnect", attributes)
 	return nil
 	return nil
 }
 }
 
 
@@ -844,14 +850,18 @@ func (daemon *Daemon) releaseNetwork(container *container.Container) {
 	}
 	}
 
 
 	sid := container.NetworkSettings.SandboxID
 	sid := container.NetworkSettings.SandboxID
-	networks := container.NetworkSettings.Networks
-	for n := range networks {
-		networks[n] = &networktypes.EndpointSettings{}
+	settings := container.NetworkSettings.Networks
+	var networks []libnetwork.Network
+	for n := range settings {
+		if nw, err := daemon.FindNetwork(n); err == nil {
+			networks = append(networks, nw)
+		}
+		settings[n] = &networktypes.EndpointSettings{}
 	}
 	}
 
 
-	container.NetworkSettings = &network.Settings{Networks: networks}
+	container.NetworkSettings = &network.Settings{Networks: settings}
 
 
-	if sid == "" || len(networks) == 0 {
+	if sid == "" || len(settings) == 0 {
 		return
 		return
 	}
 	}
 
 
@@ -864,6 +874,13 @@ func (daemon *Daemon) releaseNetwork(container *container.Container) {
 	if err := sb.Delete(); err != nil {
 	if err := sb.Delete(); err != nil {
 		logrus.Errorf("Error deleting sandbox id %s for container %s: %v", sid, container.ID, err)
 		logrus.Errorf("Error deleting sandbox id %s for container %s: %v", sid, container.ID, err)
 	}
 	}
+
+	attributes := map[string]string{
+		"container": container.ID,
+	}
+	for _, nw := range networks {
+		daemon.LogNetworkEventWithAttributes(nw, "disconnect", attributes)
+	}
 }
 }
 
 
 func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
 func (daemon *Daemon) setupIpcDirs(c *container.Container) error {

+ 5 - 0
daemon/create.go

@@ -169,5 +169,10 @@ func (daemon *Daemon) VolumeCreate(name, driverName string, opts map[string]stri
 	if (driverName != "" && v.DriverName() != driverName) || (driverName == "" && v.DriverName() != volume.DefaultDriverName) {
 	if (driverName != "" && v.DriverName() != driverName) || (driverName == "" && v.DriverName() != volume.DefaultDriverName) {
 		return nil, derr.ErrorVolumeNameTaken.WithArgs(name, v.DriverName())
 		return nil, derr.ErrorVolumeNameTaken.WithArgs(name, v.DriverName())
 	}
 	}
+
+	if driverName == "" {
+		driverName = volume.DefaultDriverName
+	}
+	daemon.LogVolumeEvent(name, "create", map[string]string{"driver": driverName})
 	return volumeToAPIType(v), nil
 	return volumeToAPIType(v), nil
 }
 }

+ 25 - 53
daemon/daemon.go

@@ -22,6 +22,7 @@ import (
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	containertypes "github.com/docker/docker/api/types/container"
 	containertypes "github.com/docker/docker/api/types/container"
+	eventtypes "github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/filters"
 	registrytypes "github.com/docker/docker/api/types/registry"
 	registrytypes "github.com/docker/docker/api/types/registry"
 	"github.com/docker/docker/api/types/strslice"
 	"github.com/docker/docker/api/types/strslice"
@@ -47,7 +48,6 @@ import (
 	"github.com/docker/docker/pkg/fileutils"
 	"github.com/docker/docker/pkg/fileutils"
 	"github.com/docker/docker/pkg/graphdb"
 	"github.com/docker/docker/pkg/graphdb"
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/pkg/idtools"
-	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/docker/docker/pkg/mount"
 	"github.com/docker/docker/pkg/mount"
 	"github.com/docker/docker/pkg/namesgenerator"
 	"github.com/docker/docker/pkg/namesgenerator"
 	"github.com/docker/docker/pkg/progress"
 	"github.com/docker/docker/pkg/progress"
@@ -554,23 +554,9 @@ func (daemon *Daemon) GetByName(name string) (*container.Container, error) {
 	return e, nil
 	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.
 // 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)
 	return daemon.EventsService.SubscribeTopic(since, sinceNano, ef)
 }
 }
 
 
@@ -580,21 +566,6 @@ func (daemon *Daemon) UnsubscribeFromEvents(listener chan interface{}) {
 	daemon.EventsService.Evict(listener)
 	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
 // children returns all child containers of the container with the
 // given name. The containers are returned as a map from the container
 // given name. The containers are returned as a map from the container
 // name to a pointer to 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 {
 	if err := daemon.referenceStore.AddTag(newTag, imageID, true); err != nil {
 		return err
 		return err
 	}
 	}
-	daemon.EventsService.Log("tag", newTag.String(), "")
+
+	daemon.LogImageEvent(imageID.String(), newTag.String(), "tag")
 	return nil
 	return nil
 }
 }
 
 
@@ -1068,15 +1040,15 @@ func (daemon *Daemon) PullImage(ref reference.Named, metaHeaders map[string][]st
 	}()
 	}()
 
 
 	imagePullConfig := &distribution.ImagePullConfig{
 	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)
 	err := distribution.Pull(ctx, ref, imagePullConfig)
@@ -1111,17 +1083,17 @@ func (daemon *Daemon) PushImage(ref reference.Named, metaHeaders map[string][]st
 	}()
 	}()
 
 
 	imagePushConfig := &distribution.ImagePushConfig{
 	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)
 	err := distribution.Push(ctx, ref, imagePushConfig)

+ 1 - 0
daemon/delete.go

@@ -157,5 +157,6 @@ func (daemon *Daemon) VolumeRm(name string) error {
 		}
 		}
 		return derr.ErrorCodeRmVolume.WithArgs(name, err)
 		return derr.ErrorCodeRmVolume.WithArgs(name, err)
 	}
 	}
+	daemon.LogVolumeEvent(v.Name(), "destroy", map[string]string{"driver": v.DriverName()})
 	return nil
 	return nil
 }
 }

+ 72 - 5
daemon/events.go

@@ -1,14 +1,81 @@
 package daemon
 package daemon
 
 
 import (
 import (
+	"strings"
+
+	"github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/container"
+	"github.com/docker/libnetwork"
 )
 )
 
 
 // LogContainerEvent generates an event related to a container.
 // LogContainerEvent generates an event related to a container.
 func (daemon *Daemon) LogContainerEvent(container *container.Container, action string) {
 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
 }
 }

+ 30 - 12
daemon/events/events.go

@@ -4,7 +4,7 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
-	"github.com/docker/docker/pkg/jsonmessage"
+	eventtypes "github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/pkg/pubsub"
 	"github.com/docker/docker/pkg/pubsub"
 )
 )
 
 
@@ -13,17 +13,17 @@ const (
 	bufferSize  = 1024
 	bufferSize  = 1024
 )
 )
 
 
-// Events is pubsub channel for *jsonmessage.JSONMessage
+// Events is pubsub channel for events generated by the engine.
 type Events struct {
 type Events struct {
 	mu     sync.Mutex
 	mu     sync.Mutex
-	events []*jsonmessage.JSONMessage
+	events []eventtypes.Message
 	pub    *pubsub.Publisher
 	pub    *pubsub.Publisher
 }
 }
 
 
 // New returns new *Events instance
 // New returns new *Events instance
 func New() *Events {
 func New() *Events {
 	return &Events{
 	return &Events{
-		events: make([]*jsonmessage.JSONMessage, 0, eventsLimit),
+		events: make([]eventtypes.Message, 0, eventsLimit),
 		pub:    pubsub.NewPublisher(100*time.Millisecond, bufferSize),
 		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
 // 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
 // of interface{}, so you need type assertion), and a function to call
 // to stop the stream of events.
 // 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()
 	e.mu.Lock()
-	current := make([]*jsonmessage.JSONMessage, len(e.events))
+	current := make([]eventtypes.Message, len(e.events))
 	copy(current, e.events)
 	copy(current, e.events)
 	l := e.pub.Subscribe()
 	l := e.pub.Subscribe()
 	e.mu.Unlock()
 	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
 // SubscribeTopic adds new listener to events, returns slice of 64 stored
 // last events, a channel in which you can expect new events (in form
 // last events, a channel in which you can expect new events (in form
 // of interface{}, so you need type assertion).
 // 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()
 	e.mu.Lock()
 	defer e.mu.Unlock()
 	defer e.mu.Unlock()
 
 
-	var buffered []*jsonmessage.JSONMessage
+	var buffered []eventtypes.Message
 	topic := func(m interface{}) bool {
 	topic := func(m interface{}) bool {
-		return ef.Include(m.(*jsonmessage.JSONMessage))
+		return ef.Include(m.(eventtypes.Message))
 	}
 	}
 
 
 	if since != -1 {
 	if since != -1 {
@@ -64,7 +64,7 @@ func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]*jsonmess
 				break
 				break
 			}
 			}
 			if ef.filter.Len() == 0 || topic(ev) {
 			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
 // Log broadcasts event to listeners. Each listener has 100 millisecond for
 // receiving event or it will be skipped.
 // 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()
 	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()
 	e.mu.Lock()
 	if len(e.events) == cap(e.events) {
 	if len(e.events) == cap(e.events) {
 		// discard oldest event
 		// discard oldest event

+ 26 - 9
daemon/events/events_test.go

@@ -5,7 +5,7 @@ import (
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
-	"github.com/docker/docker/pkg/jsonmessage"
+	"github.com/docker/docker/api/types/events"
 )
 )
 
 
 func TestEventsLog(t *testing.T) {
 func TestEventsLog(t *testing.T) {
@@ -18,10 +18,14 @@ func TestEventsLog(t *testing.T) {
 	if count != 2 {
 	if count != 2 {
 		t.Fatalf("Must be 2 subscribers, got %d", count)
 		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 {
 	select {
 	case msg := <-l1:
 	case msg := <-l1:
-		jmsg, ok := msg.(*jsonmessage.JSONMessage)
+		jmsg, ok := msg.(events.Message)
 		if !ok {
 		if !ok {
 			t.Fatalf("Unexpected type %T", msg)
 			t.Fatalf("Unexpected type %T", msg)
 		}
 		}
@@ -42,7 +46,7 @@ func TestEventsLog(t *testing.T) {
 	}
 	}
 	select {
 	select {
 	case msg := <-l2:
 	case msg := <-l2:
-		jmsg, ok := msg.(*jsonmessage.JSONMessage)
+		jmsg, ok := msg.(events.Message)
 		if !ok {
 		if !ok {
 			t.Fatalf("Unexpected type %T", msg)
 			t.Fatalf("Unexpected type %T", msg)
 		}
 		}
@@ -70,7 +74,10 @@ func TestEventsLogTimeout(t *testing.T) {
 
 
 	c := make(chan struct{})
 	c := make(chan struct{})
 	go func() {
 	go func() {
-		e.Log("test", "cont", "image")
+		actor := events.Actor{
+			ID: "image",
+		}
+		e.Log("test", events.ImageEventType, actor)
 		close(c)
 		close(c)
 	}()
 	}()
 
 
@@ -88,7 +95,12 @@ func TestLogEvents(t *testing.T) {
 		action := fmt.Sprintf("action_%d", i)
 		action := fmt.Sprintf("action_%d", i)
 		id := fmt.Sprintf("cont_%d", i)
 		id := fmt.Sprintf("cont_%d", i)
 		from := fmt.Sprintf("image_%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)
 	time.Sleep(50 * time.Millisecond)
 	current, l, _ := e.Subscribe()
 	current, l, _ := e.Subscribe()
@@ -97,16 +109,21 @@ func TestLogEvents(t *testing.T) {
 		action := fmt.Sprintf("action_%d", num)
 		action := fmt.Sprintf("action_%d", num)
 		id := fmt.Sprintf("cont_%d", num)
 		id := fmt.Sprintf("cont_%d", num)
 		from := fmt.Sprintf("image_%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 {
 	if len(e.events) != eventsLimit {
 		t.Fatalf("Must be %d events, got %d", eventsLimit, len(e.events))
 		t.Fatalf("Must be %d events, got %d", eventsLimit, len(e.events))
 	}
 	}
 
 
-	var msgs []*jsonmessage.JSONMessage
+	var msgs []events.Message
 	for len(msgs) < 10 {
 	for len(msgs) < 10 {
 		m := <-l
 		m := <-l
-		jm, ok := (m).(*jsonmessage.JSONMessage)
+		jm, ok := (m).(events.Message)
 		if !ok {
 		if !ok {
 			t.Fatalf("Unexpected type %T", m)
 			t.Fatalf("Unexpected type %T", m)
 		}
 		}

+ 49 - 19
daemon/events/filter.go

@@ -1,46 +1,76 @@
 package events
 package events
 
 
 import (
 import (
+	"github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/filters"
-	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/docker/docker/reference"
 	"github.com/docker/docker/reference"
 )
 )
 
 
 // Filter can filter out docker events from a stream
 // Filter can filter out docker events from a stream
 type Filter struct {
 type Filter struct {
-	filter    filters.Args
-	getLabels func(id string) map[string]string
+	filter filters.Args
 }
 }
 
 
 // NewFilter creates a new Filter
 // 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
 // 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 {
+	return ef.filter.ExactMatch("event", ev.Action) &&
+		ef.filter.ExactMatch("type", ev.Type) &&
+		ef.matchContainer(ev) &&
+		ef.matchVolume(ev) &&
+		ef.matchNetwork(ev) &&
+		ef.matchImage(ev) &&
+		ef.matchLabels(ev.Actor.Attributes)
 }
 }
 
 
-func (ef *Filter) isLabelFieldIncluded(id string) bool {
+func (ef *Filter) matchLabels(attributes map[string]string) bool {
 	if !ef.filter.Include("label") {
 	if !ef.filter.Include("label") {
 		return true
 		return true
 	}
 	}
-	return ef.filter.MatchKVList("label", ef.getLabels(id))
+	return ef.filter.MatchKVList("label", attributes)
 }
 }
 
 
-// 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
+func (ef *Filter) matchContainer(ev events.Message) bool {
+	return ef.fuzzyMatchName(ev, events.ContainerEventType)
+}
+
+func (ef *Filter) matchVolume(ev events.Message) bool {
+	return ef.fuzzyMatchName(ev, events.VolumeEventType)
+}
+
+func (ef *Filter) matchNetwork(ev events.Message) bool {
+	return ef.fuzzyMatchName(ev, events.NetworkEventType)
+}
+
+func (ef *Filter) fuzzyMatchName(ev events.Message, eventType string) bool {
+	return ef.filter.FuzzyMatch(eventType, ev.Actor.ID) ||
+		ef.filter.FuzzyMatch(eventType, ev.Actor.Attributes["name"])
+}
+
+// matchImage matches against both event.Actor.ID (for image events)
+// and event.Actor.Attributes["image"] (for container events), so that any container that was created
 // from an image will be included in the image events. Also compare both
 // from an image will be included in the image events. Also compare both
 // against the stripped repo name without any tags.
 // 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) matchImage(ev events.Message) bool {
+	id := ev.Actor.ID
+	nameAttr := "image"
+	var imageName string
+
+	if ev.Type == events.ImageEventType {
+		nameAttr = "name"
+	}
+
+	if n, ok := ev.Actor.Attributes[nameAttr]; 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 {
 func stripTag(image string) string {

+ 36 - 0
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)
+	}
+}

+ 4 - 4
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()}
 		untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
 
 
-		daemon.EventsService.Log("untag", imgID.String(), "")
+		daemon.LogImageEvent(imgID.String(), imgID.String(), "untag")
 		records = append(records, untaggedRecord)
 		records = append(records, untaggedRecord)
 
 
 		// If has remaining references then untag finishes the remove
 		// 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()}
 			untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
 
 
-			daemon.EventsService.Log("untag", imgID.String(), "")
+			daemon.LogImageEvent(imgID.String(), imgID.String(), "untag")
 			records = append(records, untaggedRecord)
 			records = append(records, untaggedRecord)
 		}
 		}
 	}
 	}
@@ -174,7 +174,7 @@ func (daemon *Daemon) removeAllReferencesToImageID(imgID image.ID, records *[]ty
 
 
 		untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
 		untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
 
 
-		daemon.EventsService.Log("untag", imgID.String(), "")
+		daemon.LogImageEvent(imgID.String(), imgID.String(), "untag")
 		*records = append(*records, untaggedRecord)
 		*records = append(*records, untaggedRecord)
 	}
 	}
 
 
@@ -243,7 +243,7 @@ func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDe
 		return err
 		return err
 	}
 	}
 
 
-	daemon.EventsService.Log("delete", imgID.String(), "")
+	daemon.LogImageEvent(imgID.String(), imgID.String(), "delete")
 	*records = append(*records, types.ImageDelete{Deleted: imgID.String()})
 	*records = append(*records, types.ImageDelete{Deleted: imgID.String()})
 	for _, removedLayer := range removedLayers {
 	for _, removedLayer := range removedLayers {
 		*records = append(*records, types.ImageDelete{Deleted: removedLayer.ChainID.String()})
 		*records = append(*records, types.ImageDelete{Deleted: removedLayer.ChainID.String()})

+ 1 - 1
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()))
 	outStream.Write(sf.FormatStatus("", id.String()))
 	return nil
 	return nil
 }
 }

+ 27 - 1
daemon/network.go

@@ -7,6 +7,8 @@ import (
 	"strings"
 	"strings"
 
 
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/network"
+	derr "github.com/docker/docker/errors"
+	"github.com/docker/docker/runconfig"
 	"github.com/docker/libnetwork"
 	"github.com/docker/libnetwork"
 )
 )
 
 
@@ -114,7 +116,13 @@ func (daemon *Daemon) CreateNetwork(name, driver string, ipam network.IPAM, opti
 
 
 	nwOptions = append(nwOptions, libnetwork.NetworkOptionIpam(ipam.Driver, "", v4Conf, v6Conf))
 	nwOptions = append(nwOptions, libnetwork.NetworkOptionIpam(ipam.Driver, "", v4Conf, v6Conf))
 	nwOptions = append(nwOptions, libnetwork.NetworkOptionDriverOpts(options))
 	nwOptions = append(nwOptions, libnetwork.NetworkOptionDriverOpts(options))
-	return c.NewNetwork(driver, name, nwOptions...)
+	n, err := c.NewNetwork(driver, name, nwOptions...)
+	if err != nil {
+		return nil, err
+	}
+
+	daemon.LogNetworkEvent(n, "create")
+	return n, nil
 }
 }
 
 
 func getIpamConfig(data []network.IPAMConfig) ([]*libnetwork.IpamConf, []*libnetwork.IpamConf, error) {
 func getIpamConfig(data []network.IPAMConfig) ([]*libnetwork.IpamConf, []*libnetwork.IpamConf, error) {
@@ -178,3 +186,21 @@ func (daemon *Daemon) GetNetworkDriverList() map[string]bool {
 
 
 	return pluginList
 	return pluginList
 }
 }
+
+// DeleteNetwork destroys a network unless it's one of docker's predefined networks.
+func (daemon *Daemon) DeleteNetwork(networkID string) error {
+	nw, err := daemon.FindNetwork(networkID)
+	if err != nil {
+		return err
+	}
+
+	if runconfig.IsPreDefinedNetwork(nw.Name()) {
+		return derr.ErrorCodeCantDeletePredefinedNetwork.WithArgs(nw.Name())
+	}
+
+	if err := nw.Delete(); err != nil {
+		return err
+	}
+	daemon.LogNetworkEvent(nw, "destroy")
+	return nil
+}

+ 1 - 1
daemon/start.go

@@ -156,7 +156,7 @@ func (daemon *Daemon) Cleanup(container *container.Container) {
 		daemon.unregisterExecCommand(container, eConfig)
 		daemon.unregisterExecCommand(container, eConfig)
 	}
 	}
 
 
-	if err := container.UnmountVolumes(false); err != nil {
+	if err := container.UnmountVolumes(false, daemon.LogVolumeEvent); err != nil {
 		logrus.Warnf("%s cleanup: Failed to umount volumes: %v", container.ID, err)
 		logrus.Warnf("%s cleanup: Failed to umount volumes: %v", container.ID, err)
 	}
 	}
 }
 }

+ 2 - 0
daemon/update.go

@@ -54,5 +54,7 @@ func (daemon *Daemon) update(name string, hostConfig *container.HostConfig) erro
 		}
 		}
 	}
 	}
 
 
+	daemon.LogContainerEvent(container, "update")
+
 	return nil
 	return nil
 }
 }

+ 11 - 0
daemon/volumes_unix.go

@@ -5,6 +5,7 @@ package daemon
 import (
 import (
 	"os"
 	"os"
 	"sort"
 	"sort"
+	"strconv"
 
 
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/daemon/execdriver"
 	"github.com/docker/docker/daemon/execdriver"
@@ -30,6 +31,16 @@ func (daemon *Daemon) setupMounts(container *container.Container) ([]execdriver.
 				Writable:    m.RW,
 				Writable:    m.RW,
 				Propagation: m.Propagation,
 				Propagation: m.Propagation,
 			}
 			}
+			if m.Volume != nil {
+				attributes := map[string]string{
+					"driver":      m.Volume.DriverName(),
+					"container":   container.ID,
+					"destination": m.Destination,
+					"read/write":  strconv.FormatBool(m.RW),
+					"propagation": m.Propagation,
+				}
+				daemon.LogVolumeEvent(m.Volume.Name(), "mount", attributes)
+			}
 			mounts = append(mounts, mnt)
 			mounts = append(mounts, mnt)
 		}
 		}
 	}
 	}

+ 3 - 4
distribution/pull.go

@@ -8,7 +8,6 @@ import (
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/daemon/events"
 	"github.com/docker/docker/distribution/metadata"
 	"github.com/docker/docker/distribution/metadata"
 	"github.com/docker/docker/distribution/xfer"
 	"github.com/docker/docker/distribution/xfer"
 	"github.com/docker/docker/image"
 	"github.com/docker/docker/image"
@@ -32,8 +31,8 @@ type ImagePullConfig struct {
 	// RegistryService is the registry service to use for TLS configuration
 	// RegistryService is the registry service to use for TLS configuration
 	// and endpoint lookup.
 	// and endpoint lookup.
 	RegistryService *registry.Service
 	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
 	// MetadataStore is the storage backend for distribution-specific
 	// metadata.
 	// metadata.
 	MetadataStore metadata.Store
 	MetadataStore metadata.Store
@@ -161,7 +160,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
 		return nil
 	}
 	}
 
 

+ 3 - 4
distribution/push.go

@@ -9,7 +9,6 @@ import (
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/distribution/digest"
 	"github.com/docker/distribution/digest"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/daemon/events"
 	"github.com/docker/docker/distribution/metadata"
 	"github.com/docker/docker/distribution/metadata"
 	"github.com/docker/docker/distribution/xfer"
 	"github.com/docker/docker/distribution/xfer"
 	"github.com/docker/docker/image"
 	"github.com/docker/docker/image"
@@ -35,8 +34,8 @@ type ImagePushConfig struct {
 	// RegistryService is the registry service to use for TLS configuration
 	// RegistryService is the registry service to use for TLS configuration
 	// and endpoint lookup.
 	// and endpoint lookup.
 	RegistryService *registry.Service
 	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
 	// MetadataStore is the storage backend for distribution-specific
 	// metadata.
 	// metadata.
 	MetadataStore metadata.Store
 	MetadataStore metadata.Store
@@ -156,7 +155,7 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
 			return err
 			return err
 		}
 		}
 
 
-		imagePushConfig.EventsService.Log("push", repoInfo.Name(), "")
+		imagePushConfig.ImageEventLogger(ref.String(), repoInfo.Name(), "push")
 		return nil
 		return nil
 	}
 	}
 
 

+ 6 - 0
docs/misc/deprecated.md

@@ -12,6 +12,12 @@ parent = "mn_use_docker"
 
 
 The following list of features are deprecated.
 The following list of features are deprecated.
 
 
+### Ambiguous event fields in API
+**Deprecated In Release: v1.10**
+
+The fields `ID`, `Status` and `From` in the events API have been deprecated in favor of a more rich structure.
+See the events API documentation for the new format.
+
 ### `-f` flag on `docker tag`
 ### `-f` flag on `docker tag`
 **Deprecated In Release: v1.10**
 **Deprecated In Release: v1.10**
 
 

+ 56 - 8
docs/reference/api/docker_remote_api_v1.22.md

@@ -2274,17 +2274,24 @@ Status Codes:
 
 
 `GET /events`
 `GET /events`
 
 
-Get container events from docker, either in real time via streaming, or via
-polling (using since).
+Get container events from docker, either in real time via streaming, or via polling (using since).
 
 
 Docker containers report the following events:
 Docker containers report the following events:
 
 
-    attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause
+    attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause, update
 
 
-and Docker images report:
+Docker images report the following events:
 
 
     delete, import, pull, push, tag, untag
     delete, import, pull, push, tag, untag
 
 
+Docker volumes report the following events:
+
+    create, mount, unmount, destroy
+
+Docker networks report the following events:
+
+    create, connect, disconnect, destroy
+
 **Example request**:
 **Example request**:
 
 
     GET /events?since=1374067924
     GET /events?since=1374067924
@@ -2294,10 +2301,48 @@ and Docker images report:
     HTTP/1.1 200 OK
     HTTP/1.1 200 OK
     Content-Type: application/json
     Content-Type: application/json
 
 
-    {"status":"pull","id":"busybox:latest","time":1442421700,"timeNano":1442421700598988358}
-    {"status":"create","id":"5745704abe9caa5","from":"busybox","time":1442421716,"timeNano":1442421716853979870}
-    {"status":"attach","id":"5745704abe9caa5","from":"busybox","time":1442421716,"timeNano":1442421716894759198}
-    {"status":"start","id":"5745704abe9caa5","from":"busybox","time":1442421716,"timeNano":1442421716983607193}
+    [
+	    {
+		"action": "pull",
+		"type": "image", 
+		"actor": {
+			"id": "busybox:latest",
+			"attributes": {}
+		}
+		"time": 1442421700,
+		"timeNano": 1442421700598988358
+	    },
+            {
+		"action": "create",
+		"type": "container",
+		"actor": {
+			"id": "5745704abe9caa5",
+			"attributes": {"image": "busybox"}
+		}
+		"time": 1442421716,
+		"timeNano": 1442421716853979870
+	    },
+            {
+		"action": "attach",
+		"type": "container",
+		"actor": {
+			"id": "5745704abe9caa5",
+			"attributes": {"image": "busybox"}
+		}
+		"time": 1442421716,
+		"timeNano": 1442421716894759198
+	    },
+            {
+		"action": "start",
+		"type": "container",
+		"actor": {
+			"id": "5745704abe9caa5",
+			"attributes": {"image": "busybox"}
+		}
+		"time": 1442421716,
+		"timeNano": 1442421716983607193
+	    }
+    ]
 
 
 Query Parameters:
 Query Parameters:
 
 
@@ -2308,6 +2353,9 @@ Query Parameters:
   -   `event=<string>`; -- event to filter
   -   `event=<string>`; -- event to filter
   -   `image=<string>`; -- image to filter
   -   `image=<string>`; -- image to filter
   -   `label=<string>`; -- image and container label to filter
   -   `label=<string>`; -- image and container label to filter
+  -   `type=<string>`; -- either `container` or `image` or `volume` or `network`
+  -   `volume=<string>`; -- volume to filter
+  -   `network=<string>`; -- network to filter
 
 
 Status Codes:
 Status Codes:
 
 

+ 63 - 42
docs/reference/commandline/events.md

@@ -19,14 +19,22 @@ parent = "smn_cli"
       --since=""         Show all events created since timestamp
       --since=""         Show all events created since timestamp
       --until=""         Stream events until this timestamp
       --until=""         Stream events until this timestamp
 
 
-Docker containers will report the following events:
+Docker containers report the following events:
 
 
-    attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause
+    attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause, update
 
 
-and Docker images will report:
+Docker images report the following events:
 
 
     delete, import, pull, push, tag, untag
     delete, import, pull, push, tag, untag
 
 
+Docker volumes report the following events:
+
+    create, mount, unmount, destroy
+
+Docker networks report the following events:
+
+    create, connect, disconnect, destroy
+
 The `--since` and `--until` parameters can be Unix timestamps, date formatted
 The `--since` and `--until` parameters can be Unix timestamps, date formatted
 timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed
 timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed
 relative to the client machine’s time. If you do not provide the --since option,
 relative to the client machine’s time. If you do not provide the --since option,
@@ -57,9 +65,12 @@ container container 588a23dac085 *AND* the event type is *start*
 The currently supported filters are:
 The currently supported filters are:
 
 
 * container (`container=<name or id>`)
 * container (`container=<name or id>`)
-* event (`event=<event type>`)
+* event (`event=<event action>`)
 * image (`image=<tag or id>`)
 * image (`image=<tag or id>`)
 * label (`label=<key>` or `label=<key>=<value>`)
 * label (`label=<key>` or `label=<key>=<value>`)
+* type (`type=<container or image or volume or network>`)
+* volume (`volume=<name or id>`)
+* network (`network=<name or id>`)
 
 
 ## Examples
 ## Examples
 
 
@@ -77,68 +88,78 @@ You'll need two shells for this example.
 
 
 **Shell 1: (Again .. now showing events):**
 **Shell 1: (Again .. now showing events):**
 
 
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop
-    2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die
-    2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2015-05-12T11:51:30.999999999Z07:00 container start 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8)
+    2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
 
 
 **Show events in the past from a specified time:**
 **Show events in the past from a specified time:**
 
 
     $ docker events --since 1378216169
     $ docker events --since 1378216169
-    2014-03-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop
-    2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die
-    2014-03-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8)
+    2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
 
 
     $ docker events --since '2013-09-03'
     $ docker events --since '2013-09-03'
-    2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start
-    2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop
-    2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die
-    2014-09-03T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2015-05-12T11:51:30.999999999Z07:00 container start 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8)
+    2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
 
 
     $ docker events --since '2013-09-03T15:49:29'
     $ docker events --since '2013-09-03T15:49:29'
-    2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop
-    2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die
-    2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8)
+    2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
 
 
 This example outputs all events that were generated in the last 3 minutes,
 This example outputs all events that were generated in the last 3 minutes,
 relative to the current time on the client machine:
 relative to the current time on the client machine:
 
 
     $ docker events --since '3m'
     $ docker events --since '3m'
-    2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die
-    2015-05-12T15:52:12.999999999Z07:00 4 4386fb97867d: (from ubuntu-1:14.04) stop
-    2015-05-12T15:53:45.999999999Z07:00  7805c1d35632: (from redis:2.8) die
-    2015-05-12T15:54:03.999999999Z07:00  7805c1d35632: (from redis:2.8) stop
+    2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
+    2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8)
+    2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
 
 
 **Filter events:**
 **Filter events:**
 
 
     $ docker events --filter 'event=stop'
     $ docker events --filter 'event=stop'
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop
-    2014-09-03T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
+    2014-09-03T17:42:14.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
 
 
     $ docker events --filter 'image=ubuntu-1:14.04'
     $ docker events --filter 'image=ubuntu-1:14.04'
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop
+    2014-05-10T17:42:14.999999999Z07:00 container start 4386fb97867d (image=ubuntu-1:14.04)
+    2014-05-10T17:42:14.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04)
+    2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
 
 
     $ docker events --filter 'container=7805c1d35632'
     $ docker events --filter 'container=7805c1d35632'
-    2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die
-    2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8)
+    2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image= redis:2.8)
 
 
     $ docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d'
     $ docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d'
-    2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop
-    2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die
-    2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04)
+    2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
+    2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8)
+    2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
 
 
     $ docker events --filter 'container=7805c1d35632' --filter 'event=stop'
     $ docker events --filter 'container=7805c1d35632' --filter 'event=stop'
-    2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
 
 
     $ docker events --filter 'container=container_1' --filter 'container=container_2'
     $ docker events --filter 'container=container_1' --filter 'container=container_2'
-    2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die
-    2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop
-    2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die
-    2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop
+    2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04)
+    2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04)
+    2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (imager=redis:2.8)
+    2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8)
+
+    $ docker events --filter 'type=volume'
+    2015-12-23T21:05:28.136212689Z volume create test-event-volume-local (driver=local)
+    2015-12-23T21:05:28.383462717Z volume mount test-event-volume-local (read/write=true, container=562fe10671e9273da25eed36cdce26159085ac7ee6707105fd534866340a5025, destination=/foo, driver=local, propagation=rprivate)
+    2015-12-23T21:05:28.650314265Z volume unmount test-event-volume-local (container=562fe10671e9273da25eed36cdce26159085ac7ee6707105fd534866340a5025, driver=local)
+    2015-12-23T21:05:28.716218405Z volume destroy test-event-volume-local (driver=local)
+
+    $ docker events --filter 'type=network'
+    2015-12-23T21:38:24.705709133Z network create 8b111217944ba0ba844a65b13efcd57dc494932ee2527577758f939315ba2c5b (name=test-event-network-local, type=bridge)
+    2015-12-23T21:38:25.119625123Z network connect 8b111217944ba0ba844a65b13efcd57dc494932ee2527577758f939315ba2c5b (name=test-event-network-local, container=b4be644031a3d90b400f88ab3d4bdf4dc23adb250e696b6328b85441abe2c54e, type=bridge)

+ 9 - 0
errors/daemon.go

@@ -939,4 +939,13 @@ var (
 		Description:    "There was an error while trying to start a container",
 		Description:    "There was an error while trying to start a container",
 		HTTPStatusCode: http.StatusInternalServerError,
 		HTTPStatusCode: http.StatusInternalServerError,
 	})
 	})
+
+	// ErrorCodeCantDeletePredefinedNetwork is generated when one of the predefined networks
+	// is attempted to be deleted.
+	ErrorCodeCantDeletePredefinedNetwork = errcode.Register(errGroup, errcode.ErrorDescriptor{
+		Value:          "CANT_DELETE_PREDEFINED_NETWORK",
+		Message:        "%s is a pre-defined network and cannot be removed",
+		Description:    "Engine's predefined networks cannot be deleted",
+		HTTPStatusCode: http.StatusForbidden,
+	})
 )
 )

+ 43 - 0
integration-cli/docker_api_events_test.go

@@ -1,10 +1,16 @@
 package main
 package main
 
 
 import (
 import (
+	"encoding/json"
+	"io"
 	"net/http"
 	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/docker/docker/pkg/integration/checker"
 	"github.com/docker/docker/pkg/integration/checker"
+	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/go-check/check"
 	"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")
 		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")
+}

+ 11 - 117
integration-cli/docker_cli_build_test.go

@@ -2,7 +2,6 @@ package main
 
 
 import (
 import (
 	"archive/tar"
 	"archive/tar"
-	"bufio"
 	"bytes"
 	"bytes"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
@@ -1863,104 +1862,6 @@ func (s *DockerSuite) TestBuildForceRm(c *check.C) {
 
 
 }
 }
 
 
-// Test that an infinite sleep during a build is killed if the client disconnects.
-// This test is fairly hairy because there are lots of ways to race.
-// Strategy:
-// * Monitor the output of docker events starting from before
-// * Run a 1-year-long sleep from a docker build.
-// * When docker events sees container start, close the "docker build" command
-// * Wait for docker events to emit a dying event.
-func (s *DockerSuite) TestBuildCancellationKillsSleep(c *check.C) {
-	testRequires(c, DaemonIsLinux)
-	name := "testbuildcancellation"
-
-	// (Note: one year, will never finish)
-	ctx, err := fakeContext("FROM busybox\nRUN sleep 31536000", nil)
-	if err != nil {
-		c.Fatal(err)
-	}
-	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
-
-	stdoutBuild, err := buildCmd.StdoutPipe()
-	if err := buildCmd.Start(); err != nil {
-		c.Fatalf("failed to run build: %s", err)
-	}
-
-	matchCID := regexp.MustCompile("Running in (.+)")
-	scanner := bufio.NewScanner(stdoutBuild)
-
-	outputBuffer := new(bytes.Buffer)
-	var buildID string
-	for scanner.Scan() {
-		line := scanner.Text()
-		outputBuffer.WriteString(line)
-		outputBuffer.WriteString("\n")
-		if matches := matchCID.FindStringSubmatch(line); len(matches) > 0 {
-			buildID = matches[1]
-			break
-		}
-	}
-
-	if buildID == "" {
-		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)
-		}
-	}
-	go observer.Match(matcher)
-
-	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.
-	}
-
-	// Send a kill to the `docker build` command.
-	// Causes the underlying build to be cancelled due to socket close.
-	if err := buildCmd.Process.Kill(); err != nil {
-		c.Fatalf("error killing build command: %s", err)
-	}
-
-	// Get the exit status of `docker build`, check it exited because killed.
-	if err := buildCmd.Wait(); err != nil && !isKilled(err) {
-		c.Fatalf("wait failed during build run: %T %s", err, err)
-	}
-
-	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.
-	}
-
-}
-
 func (s *DockerSuite) TestBuildRm(c *check.C) {
 func (s *DockerSuite) TestBuildRm(c *check.C) {
 	testRequires(c, DaemonIsLinux)
 	testRequires(c, DaemonIsLinux)
 	name := "testbuildrm"
 	name := "testbuildrm"
@@ -6489,33 +6390,26 @@ func (s *DockerSuite) TestBuildNoNamedVolume(c *check.C) {
 func (s *DockerSuite) TestBuildTagEvent(c *check.C) {
 func (s *DockerSuite) TestBuildTagEvent(c *check.C) {
 	testRequires(c, DaemonIsLinux)
 	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
 	dockerFile := `FROM busybox
 	RUN echo events
 	RUN echo events
 	`
 	`
-	_, err = buildImage("test", dockerFile, false)
+	_, err := buildImage("test", dockerFile, false)
 	c.Assert(err, check.IsNil)
 	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
 // #15780

+ 88 - 0
integration-cli/docker_cli_build_unix_test.go

@@ -3,12 +3,16 @@
 package main
 package main
 
 
 import (
 import (
+	"bufio"
+	"bytes"
 	"encoding/json"
 	"encoding/json"
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"path/filepath"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"strings"
+	"time"
 
 
 	"github.com/docker/docker/pkg/integration/checker"
 	"github.com/docker/docker/pkg/integration/checker"
 	"github.com/docker/go-units"
 	"github.com/docker/go-units"
@@ -115,5 +119,89 @@ func (s *DockerSuite) TestBuildAddChangeOwnership(c *check.C) {
 	if _, err := buildImageFromContext(name, ctx, true); err != nil {
 	if _, err := buildImageFromContext(name, ctx, true); err != nil {
 		c.Fatalf("build failed to complete for TestBuildAddChangeOwnership: %v", err)
 		c.Fatalf("build failed to complete for TestBuildAddChangeOwnership: %v", err)
 	}
 	}
+}
+
+// Test that an infinite sleep during a build is killed if the client disconnects.
+// This test is fairly hairy because there are lots of ways to race.
+// Strategy:
+// * Monitor the output of docker events starting from before
+// * Run a 1-year-long sleep from a docker build.
+// * When docker events sees container start, close the "docker build" command
+// * Wait for docker events to emit a dying event.
+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 {
+		c.Fatal(err)
+	}
+	defer ctx.Close()
+
+	buildCmd := exec.Command(dockerBinary, "build", "-t", name, ".")
+	buildCmd.Dir = ctx.Dir
+
+	stdoutBuild, err := buildCmd.StdoutPipe()
+	if err := buildCmd.Start(); err != nil {
+		c.Fatalf("failed to run build: %s", err)
+	}
+
+	matchCID := regexp.MustCompile("Running in (.+)")
+	scanner := bufio.NewScanner(stdoutBuild)
+
+	outputBuffer := new(bytes.Buffer)
+	var buildID string
+	for scanner.Scan() {
+		line := scanner.Text()
+		outputBuffer.WriteString(line)
+		outputBuffer.WriteString("\n")
+		if matches := matchCID.FindStringSubmatch(line); len(matches) > 0 {
+			buildID = matches[1]
+			break
+		}
+	}
+
+	if buildID == "" {
+		c.Fatalf("Unable to find build container id in build output:\n%s", outputBuffer.String())
+	}
+
+	testActions := map[string]chan bool{
+		"start": make(chan bool),
+		"die":   make(chan bool),
+	}
+
+	matcher := matchEventLine(buildID, "container", testActions)
+	go observer.Match(matcher)
+
+	select {
+	case <-time.After(10 * time.Second):
+		observer.CheckEventError(c, buildID, "start", matcher)
+	case <-testActions["start"]:
+		// ignore, done
+	}
+
+	// Send a kill to the `docker build` command.
+	// Causes the underlying build to be cancelled due to socket close.
+	if err := buildCmd.Process.Kill(); err != nil {
+		c.Fatalf("error killing build command: %s", err)
+	}
+
+	// Get the exit status of `docker build`, check it exited because killed.
+	if err := buildCmd.Wait(); err != nil && !isKilled(err) {
+		c.Fatalf("wait failed during build run: %T %s", err, err)
+	}
+
+	select {
+	case <-time.After(10 * time.Second):
+		observer.CheckEventError(c, buildID, "die", matcher)
+	case <-testActions["die"]:
+		// ignore, done
+	}
 }
 }

+ 138 - 179
integration-cli/docker_cli_events_test.go

@@ -7,7 +7,6 @@ import (
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
-	"regexp"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -67,22 +66,31 @@ func (s *DockerSuite) TestEventsUntag(c *check.C) {
 }
 }
 
 
 func (s *DockerSuite) TestEventsContainerFailStartDie(c *check.C) {
 func (s *DockerSuite) TestEventsContainerFailStartDie(c *check.C) {
-
 	out, _ := dockerCmd(c, "images", "-q")
 	out, _ := dockerCmd(c, "images", "-q")
 	image := strings.Split(out, "\n")[0]
 	image := strings.Split(out, "\n")[0]
 	_, _, err := dockerCmdWithError("run", "--name", "testeventdie", image, "blerg")
 	_, _, err := dockerCmdWithError("run", "--name", "testeventdie", image, "blerg")
 	c.Assert(err, checker.NotNil, check.Commentf("Container run with command blerg should have failed, but it did not, out=%s", out))
 	c.Assert(err, checker.NotNil, check.Commentf("Container run with command blerg should have failed, but it did not, out=%s", out))
 
 
 	out, _ = dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
 	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))
+	events := strings.Split(strings.TrimSpace(out), "\n")
 
 
+	nEvents := len(events)
+	c.Assert(nEvents, checker.GreaterOrEqualThan, 1) //Missing expected event
+
+	actions := eventActionsByIDAndType(c, events, "testeventdie", "container")
+
+	var startEvent bool
+	var dieEvent bool
+	for _, a := range actions {
+		switch a {
+		case "start":
+			startEvent = true
+		case "die":
+			dieEvent = true
+		}
+	}
+	c.Assert(startEvent, checker.True, check.Commentf("Start event not found: %v\n%v", actions, events))
+	c.Assert(dieEvent, checker.True, check.Commentf("Die event not found: %v\n%v", actions, events))
 }
 }
 
 
 func (s *DockerSuite) TestEventsLimit(c *check.C) {
 func (s *DockerSuite) TestEventsLimit(c *check.C) {
@@ -114,65 +122,44 @@ func (s *DockerSuite) TestEventsLimit(c *check.C) {
 
 
 func (s *DockerSuite) TestEventsContainerEvents(c *check.C) {
 func (s *DockerSuite) TestEventsContainerEvents(c *check.C) {
 	testRequires(c, DaemonIsLinux)
 	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()))
 	out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
 	events := strings.Split(out, "\n")
 	events := strings.Split(out, "\n")
 	events = events[:len(events)-1]
 	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) {
 func (s *DockerSuite) TestEventsContainerEventsSinceUnixEpoch(c *check.C) {
 	testRequires(c, DaemonIsLinux)
 	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 := time.Unix(0, 0).Format(time.RFC3339Nano)
 	timeBeginning = strings.Replace(timeBeginning, "Z", ".000000000Z", -1)
 	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 := strings.Split(out, "\n")
 	events = events[:len(events)-1]
 	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))
-
-}
 
 
-func (s *DockerSuite) TestEventsImageUntagDelete(c *check.C) {
-	testRequires(c, DaemonIsLinux)
-	name := "testimageevents"
-	_, 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))
+	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) TestEventsImageTag(c *check.C) {
 func (s *DockerSuite) TestEventsImageTag(c *check.C) {
@@ -189,10 +176,10 @@ func (s *DockerSuite) TestEventsImageTag(c *check.C) {
 	events := strings.Split(strings.TrimSpace(out), "\n")
 	events := strings.Split(strings.TrimSpace(out), "\n")
 	c.Assert(events, checker.HasLen, 1, check.Commentf("was expecting 1 event. out=%s", out))
 	c.Assert(events, checker.HasLen, 1, check.Commentf("was expecting 1 event. out=%s", out))
 	event := strings.TrimSpace(events[0])
 	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(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) {
 func (s *DockerSuite) TestEventsImagePull(c *check.C) {
@@ -208,68 +195,45 @@ func (s *DockerSuite) TestEventsImagePull(c *check.C) {
 
 
 	events := strings.Split(strings.TrimSpace(out), "\n")
 	events := strings.Split(strings.TrimSpace(out), "\n")
 	event := strings.TrimSpace(events[len(events)-1])
 	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["id"], checker.Equals, "hello-world:latest")
+	c.Assert(matches["action"], checker.Equals, "pull")
 
 
 }
 }
 
 
 func (s *DockerSuite) TestEventsImageImport(c *check.C) {
 func (s *DockerSuite) TestEventsImageImport(c *check.C) {
 	testRequires(c, DaemonIsLinux)
 	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")
 	out, _ := dockerCmd(c, "run", "-d", "busybox", "true")
 	cleanedContainerID := strings.TrimSpace(out)
 	cleanedContainerID := strings.TrimSpace(out)
 
 
-	out, _, err = runCommandPipelineWithOutput(
+	since := daemonTime(c).Unix()
+	out, _, err := runCommandPipelineWithOutput(
 		exec.Command(dockerBinary, "export", cleanedContainerID),
 		exec.Command(dockerBinary, "export", cleanedContainerID),
 		exec.Command(dockerBinary, "import", "-"),
 		exec.Command(dockerBinary, "import", "-"),
 	)
 	)
 	c.Assert(err, checker.IsNil, check.Commentf("import failed with output: %q", out))
 	c.Assert(err, checker.IsNil, check.Commentf("import failed with output: %q", out))
 	imageRef := strings.TrimSpace(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) {
 func (s *DockerSuite) TestEventsFilters(c *check.C) {
 	testRequires(c, DaemonIsLinux)
 	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()
 	since := daemonTime(c).Unix()
 	dockerCmd(c, "run", "--rm", "busybox", "true")
 	dockerCmd(c, "run", "--rm", "busybox", "true")
 	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")
 	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")
 	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
 	// make sure we at least got 2 start events
 	count := strings.Count(out, "start")
 	count := strings.Count(out, "start")
@@ -355,7 +319,8 @@ func (s *DockerSuite) TestEventsFilterImageLabels(c *check.C) {
 		"events",
 		"events",
 		fmt.Sprintf("--since=%d", since),
 		fmt.Sprintf("--since=%d", since),
 		fmt.Sprintf("--until=%d", daemonTime(c).Unix()),
 		fmt.Sprintf("--until=%d", daemonTime(c).Unix()),
-		"--filter", fmt.Sprintf("label=%s", label))
+		"--filter", fmt.Sprintf("label=%s", label),
+		"--filter", "type=image")
 
 
 	events := strings.Split(strings.TrimSpace(out), "\n")
 	events := strings.Split(strings.TrimSpace(out), "\n")
 
 
@@ -385,15 +350,9 @@ func (s *DockerSuite) TestEventsFilterContainer(c *check.C) {
 			return fmt.Errorf("expected 4 events, got %v", events)
 			return fmt.Errorf("expected 4 events, got %v", events)
 		}
 		}
 		for _, event := range 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
 		return nil
@@ -412,72 +371,6 @@ func (s *DockerSuite) TestEventsFilterContainer(c *check.C) {
 	}
 	}
 }
 }
 
 
-func (s *DockerSuite) TestEventsStreaming(c *check.C) {
-	testRequires(c, DaemonIsLinux)
-
-	eventCreate := make(chan struct{})
-	eventStart := make(chan struct{})
-	eventDie := make(chan struct{})
-	eventDestroy := make(chan struct{})
-
-	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:latest", "true")
-	containerID := strings.TrimSpace(out)
-	matchCreate := regexp.MustCompile(containerID + `: \(from busybox:latest\) create\z`)
-	matchStart := regexp.MustCompile(containerID + `: \(from busybox:latest\) start\z`)
-	matchDie := regexp.MustCompile(containerID + `: \(from busybox:latest\) die\z`)
-	matchDestroy := regexp.MustCompile(containerID + `: \(from busybox:latest\) destroy\z`)
-
-	matcher := func(text string) {
-		switch {
-		case matchCreate.MatchString(text):
-			close(eventCreate)
-		case matchStart.MatchString(text):
-			close(eventStart)
-		case matchDie.MatchString(text):
-			close(eventDie)
-		case matchDestroy.MatchString(text):
-			close(eventDestroy)
-		}
-	}
-	go observer.Match(matcher)
-
-	select {
-	case <-time.After(5 * time.Second):
-		c.Fatal(observer.TimeoutError(containerID, "create"))
-	case <-eventCreate:
-		// ignore, done
-	}
-
-	select {
-	case <-time.After(5 * time.Second):
-		c.Fatal(observer.TimeoutError(containerID, "start"))
-	case <-eventStart:
-		// ignore, done
-	}
-
-	select {
-	case <-time.After(5 * time.Second):
-		c.Fatal(observer.TimeoutError(containerID, "die"))
-	case <-eventDie:
-		// ignore, done
-	}
-
-	dockerCmd(c, "rm", containerID)
-
-	select {
-	case <-time.After(5 * time.Second):
-		c.Fatal(observer.TimeoutError(containerID, "destroy"))
-	case <-eventDestroy:
-		// ignore, done
-	}
-}
-
 func (s *DockerSuite) TestEventsCommit(c *check.C) {
 func (s *DockerSuite) TestEventsCommit(c *check.C) {
 	testRequires(c, DaemonIsLinux)
 	testRequires(c, DaemonIsLinux)
 	since := daemonTime(c).Unix()
 	since := daemonTime(c).Unix()
@@ -490,7 +383,7 @@ func (s *DockerSuite) TestEventsCommit(c *check.C) {
 	dockerCmd(c, "stop", cID)
 	dockerCmd(c, "stop", cID)
 
 
 	out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since)))
 	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) {
 func (s *DockerSuite) TestEventsCopy(c *check.C) {
@@ -515,12 +408,12 @@ func (s *DockerSuite) TestEventsCopy(c *check.C) {
 	dockerCmd(c, "cp", "cptest:/tmp/file", tempFile.Name())
 	dockerCmd(c, "cp", "cptest:/tmp/file", tempFile.Name())
 
 
 	out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since)))
 	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")
 	dockerCmd(c, "cp", tempFile.Name(), "cptest:/tmp/filecopy")
 
 
 	out, _ = dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since)))
 	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) {
 func (s *DockerSuite) TestEventsResize(c *check.C) {
@@ -539,7 +432,7 @@ func (s *DockerSuite) TestEventsResize(c *check.C) {
 	dockerCmd(c, "stop", cID)
 	dockerCmd(c, "stop", cID)
 
 
 	out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since)))
 	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) {
 func (s *DockerSuite) TestEventsAttach(c *check.C) {
@@ -571,7 +464,7 @@ func (s *DockerSuite) TestEventsAttach(c *check.C) {
 	dockerCmd(c, "stop", cID)
 	dockerCmd(c, "stop", cID)
 
 
 	out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since)))
 	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) {
 func (s *DockerSuite) TestEventsRename(c *check.C) {
@@ -582,7 +475,7 @@ func (s *DockerSuite) TestEventsRename(c *check.C) {
 	dockerCmd(c, "rename", "oldName", "newName")
 	dockerCmd(c, "rename", "oldName", "newName")
 
 
 	out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=newName", "--until="+strconv.Itoa(int(since)))
 	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) {
 func (s *DockerSuite) TestEventsTop(c *check.C) {
@@ -597,7 +490,7 @@ func (s *DockerSuite) TestEventsTop(c *check.C) {
 	dockerCmd(c, "stop", cID)
 	dockerCmd(c, "stop", cID)
 
 
 	out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since)))
 	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
 // #13753
@@ -624,5 +517,71 @@ func (s *DockerRegistrySuite) TestEventsImageFilterPush(c *check.C) {
 	dockerCmd(c, "push", repoName)
 	dockerCmd(c, "push", repoName)
 
 
 	out, _ = dockerCmd(c, "events", "--since=0", "-f", "image="+repoName, "-f", "event=push", "--until="+strconv.Itoa(int(since)))
 	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))
+}
+
+func (s *DockerSuite) TestEventsFilterType(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+	since := daemonTime(c).Unix()
+	name := "labelfiltertest"
+	label := "io.docker.testing=image"
+
+	// Build a test image.
+	_, err := buildImage(name, fmt.Sprintf(`
+		FROM busybox:latest
+		LABEL %s`, label), true)
+	c.Assert(err, checker.IsNil, check.Commentf("Couldn't create image"))
+
+	dockerCmd(c, "tag", name, "labelfiltertest:tag1")
+	dockerCmd(c, "tag", name, "labelfiltertest:tag2")
+	dockerCmd(c, "tag", "busybox:latest", "labelfiltertest:tag3")
+
+	out, _ := dockerCmd(
+		c,
+		"events",
+		fmt.Sprintf("--since=%d", since),
+		fmt.Sprintf("--until=%d", daemonTime(c).Unix()),
+		"--filter", fmt.Sprintf("label=%s", label),
+		"--filter", "type=image")
+
+	events := strings.Split(strings.TrimSpace(out), "\n")
+
+	// 2 events from the "docker tag" command, another one is from "docker build"
+	c.Assert(events, checker.HasLen, 3, check.Commentf("Events == %s", events))
+	for _, e := range events {
+		c.Assert(e, checker.Contains, "labelfiltertest")
+	}
+
+	out, _ = dockerCmd(
+		c,
+		"events",
+		fmt.Sprintf("--since=%d", since),
+		fmt.Sprintf("--until=%d", daemonTime(c).Unix()),
+		"--filter", fmt.Sprintf("label=%s", label),
+		"--filter", "type=container")
+	events = strings.Split(strings.TrimSpace(out), "\n")
+
+	// Events generated by the container that builds the image
+	c.Assert(events, checker.HasLen, 3, check.Commentf("Events == %s", events))
+
+	out, _ = dockerCmd(
+		c,
+		"events",
+		fmt.Sprintf("--since=%d", since),
+		fmt.Sprintf("--until=%d", daemonTime(c).Unix()),
+		"--filter", "type=network")
+	events = strings.Split(strings.TrimSpace(out), "\n")
+	c.Assert(len(events), checker.GreaterOrEqualThan, 1, check.Commentf("Events == %s", events))
+}
+
+func (s *DockerSuite) TestEventsFilterImageInContainerAction(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+
+	since := daemonTime(c).Unix()
+	dockerCmd(c, "run", "--name", "test-container", "-d", "busybox", "true")
+	waitRun("test-container")
+
+	out, _ := dockerCmd(c, "events", "--filter", "image=busybox", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
+	events := strings.Split(strings.TrimSpace(out), "\n")
+	c.Assert(len(events), checker.GreaterThan, 1, check.Commentf(out))
 }
 }

+ 251 - 22
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()))
 	out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=oomFalse", fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
 	events := strings.Split(strings.TrimSuffix(out, "\n"), "\n")
 	events := strings.Split(strings.TrimSuffix(out, "\n"), "\n")
-	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])
-	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))
+	nEvents := len(events)
+
+	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) {
 func (s *DockerSuite) TestEventsOOMDisableTrue(c *check.C) {
@@ -98,19 +94,252 @@ func (s *DockerSuite) TestEventsOOMDisableTrue(c *check.C) {
 
 
 		out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=oomTrue", fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
 		out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=oomTrue", fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
 		events := strings.Split(strings.TrimSuffix(out, "\n"), "\n")
 		events := strings.Split(strings.TrimSuffix(out, "\n"), "\n")
-		c.Assert(len(events), 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])
+		nEvents := len(events)
+		c.Assert(nEvents, checker.GreaterOrEqualThan, 4) //Missing expected event
 
 
-		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")
 		out, _ = dockerCmd(c, "inspect", "-f", "{{.State.Status}}", "oomTrue")
 		c.Assert(strings.TrimSpace(out), checker.Equals, "running", check.Commentf("container should be still running"))
 		c.Assert(strings.TrimSpace(out), checker.Equals, "running", check.Commentf("container should be still running"))
 	}
 	}
 }
 }
+
+// #18453
+func (s *DockerSuite) TestEventsContainerFilterByName(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+	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
+func (s *DockerSuite) TestEventsContainerFilterBeforeCreate(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+	var (
+		out string
+		ch  chan struct{}
+	)
+	ch = make(chan struct{})
+
+	// calculate the time it takes to create and start a container and sleep 2 seconds
+	// this is to make sure the docker event will recevie the event of container
+	since := daemonTime(c).Unix()
+	id, _ := dockerCmd(c, "run", "-d", "busybox", "top")
+	cID := strings.TrimSpace(id)
+	waitRun(cID)
+	time.Sleep(2 * time.Second)
+	duration := daemonTime(c).Unix() - since
+
+	go func() {
+		out, _ = dockerCmd(c, "events", "-f", "container=foo", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix()+2*duration))
+		close(ch)
+	}()
+	// Sleep 2 second to wait docker event to start
+	time.Sleep(2 * time.Second)
+	id, _ = dockerCmd(c, "run", "--name=foo", "-d", "busybox", "top")
+	cID = strings.TrimSpace(id)
+	waitRun(cID)
+	<-ch
+	c.Assert(out, checker.Contains, cID, check.Commentf("Missing event of container (foo)"))
+}
+
+func (s *DockerSuite) TestVolumeEvents(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+
+	since := daemonTime(c).Unix()
+
+	// Observe create/mount volume actions
+	dockerCmd(c, "volume", "create", "--name", "test-event-volume-local")
+	dockerCmd(c, "run", "--name", "test-volume-container", "--volume", "test-event-volume-local:/foo", "-d", "busybox", "true")
+	waitRun("test-volume-container")
+
+	// Observe unmount/destroy volume actions
+	dockerCmd(c, "rm", "-f", "test-volume-container")
+	dockerCmd(c, "volume", "rm", "test-event-volume-local")
+
+	out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
+	events := strings.Split(strings.TrimSpace(out), "\n")
+	c.Assert(len(events), checker.GreaterThan, 4)
+
+	volumeEvents := eventActionsByIDAndType(c, events, "test-event-volume-local", "volume")
+	c.Assert(volumeEvents, checker.HasLen, 4)
+	c.Assert(volumeEvents[0], checker.Equals, "create")
+	c.Assert(volumeEvents[1], checker.Equals, "mount")
+	c.Assert(volumeEvents[2], checker.Equals, "unmount")
+	c.Assert(volumeEvents[3], checker.Equals, "destroy")
+}
+
+func (s *DockerSuite) TestNetworkEvents(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+
+	since := daemonTime(c).Unix()
+
+	// Observe create/connect network actions
+	dockerCmd(c, "network", "create", "test-event-network-local")
+	dockerCmd(c, "run", "--name", "test-network-container", "--net", "test-event-network-local", "-d", "busybox", "true")
+	waitRun("test-network-container")
+
+	// Observe disconnect/destroy network actions
+	dockerCmd(c, "rm", "-f", "test-network-container")
+	dockerCmd(c, "network", "rm", "test-event-network-local")
+
+	out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
+	events := strings.Split(strings.TrimSpace(out), "\n")
+	c.Assert(len(events), checker.GreaterThan, 4)
+
+	netEvents := eventActionsByIDAndType(c, events, "test-event-network-local", "network")
+	c.Assert(netEvents, checker.HasLen, 4)
+	c.Assert(netEvents[0], checker.Equals, "create")
+	c.Assert(netEvents[1], checker.Equals, "connect")
+	c.Assert(netEvents[2], checker.Equals, "disconnect")
+	c.Assert(netEvents[3], checker.Equals, "destroy")
+}
+
+func (s *DockerSuite) TestEventsStreaming(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:latest", "true")
+	containerID := strings.TrimSpace(out)
+
+	testActions := map[string]chan bool{
+		"create":  make(chan bool),
+		"start":   make(chan bool),
+		"die":     make(chan bool),
+		"destroy": make(chan bool),
+	}
+
+	matcher := matchEventLine(containerID, "container", testActions)
+	go observer.Match(matcher)
+
+	select {
+	case <-time.After(5 * time.Second):
+		observer.CheckEventError(c, containerID, "create", matcher)
+	case <-testActions["create"]:
+		// ignore, done
+	}
+
+	select {
+	case <-time.After(5 * time.Second):
+		observer.CheckEventError(c, containerID, "start", matcher)
+	case <-testActions["start"]:
+		// ignore, done
+	}
+
+	select {
+	case <-time.After(5 * time.Second):
+		observer.CheckEventError(c, containerID, "die", matcher)
+	case <-testActions["die"]:
+		// ignore, done
+	}
+
+	dockerCmd(c, "rm", containerID)
+
+	select {
+	case <-time.After(5 * time.Second):
+		observer.CheckEventError(c, containerID, "destroy", matcher)
+	case <-testActions["destroy"]:
+		// ignore, done
+	}
+}
+
+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"
+	imageID, err := buildImage(name,
+		`FROM scratch
+		MAINTAINER "docker"`,
+		true)
+	c.Assert(err, checker.IsNil)
+	c.Assert(deleteImages(name), checker.IsNil)
+
+	testActions := map[string]chan bool{
+		"untag":  make(chan bool),
+		"delete": make(chan bool),
+	}
+
+	matcher := matchEventLine(imageID, "image", testActions)
+	go observer.Match(matcher)
+
+	select {
+	case <-time.After(10 * time.Second):
+		observer.CheckEventError(c, imageID, "untag", matcher)
+	case <-testActions["untag"]:
+		// ignore, done
+	}
+
+	select {
+	case <-time.After(10 * time.Second):
+		observer.CheckEventError(c, imageID, "delete", matcher)
+	case <-testActions["delete"]:
+		// ignore, done
+	}
+}
+
+func (s *DockerSuite) TestEventsFilterVolumeAndNetworkType(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+
+	since := daemonTime(c).Unix()
+
+	dockerCmd(c, "network", "create", "test-event-network-type")
+	dockerCmd(c, "volume", "create", "--name", "test-event-volume-type")
+
+	out, _ := dockerCmd(c, "events", "--filter", "type=volume", "--filter", "type=network", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
+	events := strings.Split(strings.TrimSpace(out), "\n")
+	c.Assert(len(events), checker.GreaterOrEqualThan, 2, check.Commentf(out))
+
+	networkActions := eventActionsByIDAndType(c, events, "test-event-network-type", "network")
+	volumeActions := eventActionsByIDAndType(c, events, "test-event-volume-type", "volume")
+
+	c.Assert(volumeActions[0], checker.Equals, "create")
+	c.Assert(networkActions[0], checker.Equals, "create")
+}
+
+func (s *DockerSuite) TestEventsFilterVolumeID(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+
+	since := daemonTime(c).Unix()
+
+	dockerCmd(c, "volume", "create", "--name", "test-event-volume-id")
+	out, _ := dockerCmd(c, "events", "--filter", "volume=test-event-volume-id", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
+	events := strings.Split(strings.TrimSpace(out), "\n")
+	c.Assert(events, checker.HasLen, 1)
+
+	c.Assert(events[0], checker.Contains, "test-event-volume-id")
+	c.Assert(events[0], checker.Contains, "driver=local")
+}
+
+func (s *DockerSuite) TestEventsFilterNetworkID(c *check.C) {
+	testRequires(c, DaemonIsLinux)
+
+	since := daemonTime(c).Unix()
+
+	dockerCmd(c, "network", "create", "test-event-network-local")
+	out, _ := dockerCmd(c, "events", "--filter", "network=test-event-network-local", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
+	events := strings.Split(strings.TrimSpace(out), "\n")
+	c.Assert(events, checker.HasLen, 1)
+
+	c.Assert(events[0], checker.Contains, "test-event-network-local")
+	c.Assert(events[0], checker.Contains, "type=bridge")
+}

+ 9 - 22
integration-cli/docker_cli_pause_test.go

@@ -23,15 +23,11 @@ func (s *DockerSuite) TestPause(c *check.C) {
 	dockerCmd(c, "unpause", name)
 	dockerCmd(c, "unpause", name)
 
 
 	out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
 	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) {
 func (s *DockerSuite) TestPauseMultipleContainers(c *check.C) {
@@ -53,21 +49,12 @@ func (s *DockerSuite) TestPauseMultipleContainers(c *check.C) {
 	dockerCmd(c, append([]string{"unpause"}, containers...)...)
 	dockerCmd(c, append([]string{"unpause"}, containers...)...)
 
 
 	out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix()))
 	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")
-	}
-	for _, unpauseEvent := range unpauseEvents {
-		c.Assert(unpauseEvent[len(unpauseEvent)-1], checker.Equals, "unpause")
+		c.Assert(actions[len(actions)-2], checker.Equals, "pause")
+		c.Assert(actions[len(actions)-1], checker.Equals, "unpause")
 	}
 	}
-
 }
 }

+ 159 - 19
integration-cli/events_utils.go

@@ -6,27 +6,50 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"os/exec"
 	"os/exec"
+	"regexp"
 	"strconv"
 	"strconv"
+	"strings"
 
 
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/pkg/integration/checker"
 	"github.com/go-check/check"
 	"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<eventType>\w+)`
+	reAction     = `(?P<action>\w+)`
+	reID         = `(?P<id>[^\s]+)`
+	reAttributes = `(\s\((?P<attributes>[^\)]+)\))?`
+	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.
 // eventMatcher is a function that tries to match an event input.
-type eventMatcher func(text string)
+type eventMatcher func(text string) bool
 
 
 // eventObserver runs an events commands and observes its output.
 // eventObserver runs an events commands and observes its output.
 type eventObserver struct {
 type eventObserver struct {
-	buffer  *bytes.Buffer
-	command *exec.Cmd
-	stdout  io.Reader
+	buffer             *bytes.Buffer
+	command            *exec.Cmd
+	scanner            *bufio.Scanner
+	startTime          string
+	disconnectionError error
 }
 }
 
 
 // newEventObserver creates the observer and initializes the command
 // newEventObserver creates the observer and initializes the command
 // without running it. Users must call `eventObserver.Start` to start the command.
 // without running it. Users must call `eventObserver.Start` to start the command.
 func newEventObserver(c *check.C, args ...string) (*eventObserver, error) {
 func newEventObserver(c *check.C, args ...string) (*eventObserver, error) {
 	since := daemonTime(c).Unix()
 	since := daemonTime(c).Unix()
+	return newEventObserverWithBacklog(c, since, args...)
+}
 
 
-	cmdArgs := []string{"events", "--since", strconv.FormatInt(since, 10)}
+// 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) {
+	startTime := strconv.FormatInt(since, 10)
+	cmdArgs := []string{"events", "--since", startTime}
 	if len(args) > 0 {
 	if len(args) > 0 {
 		cmdArgs = append(cmdArgs, args...)
 		cmdArgs = append(cmdArgs, args...)
 	}
 	}
@@ -37,9 +60,10 @@ func newEventObserver(c *check.C, args ...string) (*eventObserver, error) {
 	}
 	}
 
 
 	return &eventObserver{
 	return &eventObserver{
-		buffer:  new(bytes.Buffer),
-		command: eventsCmd,
-		stdout:  stdout,
+		buffer:    new(bytes.Buffer),
+		command:   eventsCmd,
+		scanner:   bufio.NewScanner(stdout),
+		startTime: startTime,
 	}, nil
 	}, nil
 }
 }
 
 
@@ -51,28 +75,144 @@ func (e *eventObserver) Start() error {
 // Stop stops the events command.
 // Stop stops the events command.
 func (e *eventObserver) Stop() {
 func (e *eventObserver) Stop() {
 	e.command.Process.Kill()
 	e.command.Process.Kill()
+	e.command.Process.Release()
 }
 }
 
 
 // Match tries to match the events output with a given matcher.
 // Match tries to match the events output with a given matcher.
 func (e *eventObserver) Match(match eventMatcher) {
 func (e *eventObserver) Match(match eventMatcher) {
-	scanner := bufio.NewScanner(e.stdout)
-
-	for scanner.Scan() {
-		text := scanner.Text()
+	for e.scanner.Scan() {
+		text := e.scanner.Text()
 		e.buffer.WriteString(text)
 		e.buffer.WriteString(text)
 		e.buffer.WriteString("\n")
 		e.buffer.WriteString("\n")
 
 
 		match(text)
 		match(text)
 	}
 	}
+
+	err := e.scanner.Err()
+	if err == nil {
+		err = io.EOF
+	}
+
+	logrus.Debug("EventObserver scanner loop finished: %v", err)
+	e.disconnectionError = err
+}
+
+func (e *eventObserver) CheckEventError(c *check.C, id, event string, match eventMatcher) {
+	var foundEvent bool
+	scannerOut := e.buffer.String()
+
+	if e.disconnectionError != nil {
+		until := strconv.FormatInt(daemonTime(c).Unix(), 10)
+		out, _ := dockerCmd(c, "events", "--since", e.startTime, "--until", until)
+		events := strings.Split(strings.TrimSpace(out), "\n")
+		for _, e := range events {
+			if match(e) {
+				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)
+	}
 }
 }
 
 
-// TimeoutError generates an error for a given containerID and event type.
-// It attaches the events command output to the error.
-func (e *eventObserver) TimeoutError(id, event string) error {
-	return fmt.Errorf("failed to observe event `%s` for %s\n%v", event, id, e.output())
+// 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) bool {
+		matches := parseEventText(text)
+		if len(matches) == 0 {
+			return false
+		}
+
+		if matchIDAndEventType(matches, id, eventType) {
+			if ch, ok := actions[matches["action"]]; ok {
+				close(ch)
+				return true
+			}
+		}
+		return false
+	}
 }
 }
 
 
-// output returns the events command output read until now by the Match goroutine.
-func (e *eventObserver) output() string {
-	return e.buffer.String()
+// 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)
+	md := map[string]string{}
+	if len(matches) == 0 {
+		return md
+	}
+
+	names := eventCliRegexp.SubexpNames()
+	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)
+	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)
+		matched, err := regexp.MatchString(match, matches["action"])
+		c.Assert(err, checker.IsNil)
+		c.Assert(matched, checker.True, check.Commentf("Matcher: %s did not match %s", match, matches["action"]))
+	}
+}
+
+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(matchEventID(matches, id), checker.True)
+
+		matched, err := regexp.MatchString(match, matches["action"])
+		c.Assert(err, checker.IsNil)
+		c.Assert(matched, checker.True, check.Commentf("Matcher: %s did not match %s", match, matches["action"]))
+	}
 }
 }