diff --git a/api/client/events.go b/api/client/events.go index ba0227d8fb..4ceaf43f84 100644 --- a/api/client/events.go +++ b/api/client/events.go @@ -1,11 +1,18 @@ package client import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + "github.com/docker/docker/api/types" + eventtypes "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/jsonlog" flag "github.com/docker/docker/pkg/mflag" ) @@ -46,5 +53,56 @@ func (cli *DockerCli) CmdEvents(args ...string) error { } defer responseBody.Close() - return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) + return streamEvents(responseBody, cli.out) +} + +// streamEvents decodes prints the incoming events in the provided output. +func streamEvents(input io.Reader, output io.Writer) error { + 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") } diff --git a/api/client/stats.go b/api/client/stats.go index c5c9a8f7db..968d17eab4 100644 --- a/api/client/stats.go +++ b/api/client/stats.go @@ -11,8 +11,9 @@ import ( "time" "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" - "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/go-units" ) @@ -189,7 +190,11 @@ func (cli *DockerCli) CmdStats(args ...string) error { err error } 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) if err != nil { c <- watch{err: err} @@ -197,15 +202,15 @@ func (cli *DockerCli) CmdStats(args ...string) error { } 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} - 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) { cChan := make(chan watch) diff --git a/api/server/router/network/backend.go b/api/server/router/network/backend.go index 75bd12c5d8..c3d6101db6 100644 --- a/api/server/router/network/backend.go +++ b/api/server/router/network/backend.go @@ -19,4 +19,5 @@ type Backend interface { DisconnectContainerFromNetwork(containerName string, network libnetwork.Network) error NetworkControllerEnabled() bool + DeleteNetwork(name string) error } diff --git a/api/server/router/network/network_routes.go b/api/server/router/network/network_routes.go index 6c4e22e7e7..86ef6854c1 100644 --- a/api/server/router/network/network_routes.go +++ b/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 { - 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 { diff --git a/api/server/router/system/backend.go b/api/server/router/system/backend.go index 21755b78e5..bc218087d9 100644 --- a/api/server/router/system/backend.go +++ b/api/server/router/system/backend.go @@ -2,8 +2,8 @@ package system import ( "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/jsonmessage" ) // Backend is the methods that need to be implemented to provide @@ -11,7 +11,7 @@ import ( type Backend interface { SystemInfo() (*types.Info, error) SystemVersion() types.Version - SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]*jsonmessage.JSONMessage, chan interface{}) + SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{}) UnsubscribeFromEvents(chan interface{}) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error) } diff --git a/api/server/router/system/system_routes.go b/api/server/router/system/system_routes.go index 0a2e2c18c6..afd60a0042 100644 --- a/api/server/router/system/system_routes.go +++ b/api/server/router/system/system_routes.go @@ -9,10 +9,10 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" timetypes "github.com/docker/docker/api/types/time" "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/jsonmessage" "golang.org/x/net/context" ) @@ -98,8 +98,9 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r * for { select { case ev := <-l: - jev, ok := ev.(*jsonmessage.JSONMessage) + jev, ok := ev.(events.Message) if !ok { + logrus.Warnf("unexpected event message: %q", ev) continue } if err := enc.Encode(jev); err != nil { diff --git a/api/types/events/events.go b/api/types/events/events.go new file mode 100644 index 0000000000..c5987aaf14 --- /dev/null +++ b/api/types/events/events.go @@ -0,0 +1,38 @@ +package events + +const ( + // ContainerEventType is the event type that containers generate + ContainerEventType = "container" + // ImageEventType is the event type that images generate + ImageEventType = "image" + // VolumeEventType is the event type that volumes generate + VolumeEventType = "volume" + // NetworkEventType is the event type that networks generate + NetworkEventType = "network" +) + +// Actor describes something that generates events, +// like a container, or a network, or a volume. +// It has a defined name and a set or attributes. +// The container attributes are its labels, other actors +// can generate these attributes from other properties. +type Actor struct { + ID string + Attributes map[string]string +} + +// Message represents the information an event contains +type Message struct { + // Deprecated information from JSONMessage. + // With data only in container events. + Status string `json:"status,omitempty"` + ID string `json:"id,omitempty"` + From string `json:"from,omitempty"` + + Type string + Action string + Actor Actor + + Time int64 `json:"time,omitempty"` + TimeNano int64 `json:"timeNano,omitempty"` +} diff --git a/api/types/filters/parse.go b/api/types/filters/parse.go index 6c423ceae8..e99462c0a8 100644 --- a/api/types/filters/parse.go +++ b/api/types/filters/parse.go @@ -197,6 +197,22 @@ func (filters Args) ExactMatch(field, source string) bool { return false } +// FuzzyMatch returns true if the source matches exactly one of the filters, +// or the source has one of the filters as a prefix. +func (filters Args) FuzzyMatch(field, source string) bool { + if filters.ExactMatch(field, source) { + return true + } + + fieldValues := filters.fields[field] + for prefix := range fieldValues { + if strings.HasPrefix(source, prefix) { + return true + } + } + return false +} + // Include returns true if the name of the field to filter is in the filters. func (filters Args) Include(field string) bool { _, ok := filters.fields[field] diff --git a/api/types/filters/parse_test.go b/api/types/filters/parse_test.go index 308d1bcdb8..4d5c814551 100644 --- a/api/types/filters/parse_test.go +++ b/api/types/filters/parse_test.go @@ -349,3 +349,21 @@ func TestWalkValues(t *testing.T) { t.Fatalf("Expected to not iterate when the field doesn't exist, got %v", err) } } + +func TestFuzzyMatch(t *testing.T) { + f := NewArgs() + f.Add("container", "foo") + + cases := map[string]bool{ + "foo": true, + "foobar": true, + "barfoo": false, + "bar": false, + } + for source, match := range cases { + got := f.FuzzyMatch("container", source) + if got != match { + t.Fatalf("Expected %v, got %v: %s", match, got, source) + } + } +} diff --git a/container/container_unix.go b/container/container_unix.go index 0c3011d004..c9afcaa0c0 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -618,7 +618,7 @@ func detachMounted(path string) error { } // 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 ( volumeMounts []volume.MountPoint err error @@ -649,6 +649,12 @@ func (container *Container) UnmountVolumes(forceSyscall bool) error { if err := volumeMount.Volume.Unmount(); err != nil { return err } + + attributes := map[string]string{ + "driver": volumeMount.Volume.DriverName(), + "container": container.ID, + } + volumeEventLog(volumeMount.Volume.Name(), "unmount", attributes) } } diff --git a/container/container_windows.go b/container/container_windows.go index 87d59c07bf..cd8134472f 100644 --- a/container/container_windows.go +++ b/container/container_windows.go @@ -39,7 +39,7 @@ func (container *Container) IpcMounts() []execdriver.Mount { } // 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 } diff --git a/daemon/archive.go b/daemon/archive.go index de329741c2..1c3cec0e9d 100644 --- a/daemon/archive.go +++ b/daemon/archive.go @@ -84,7 +84,7 @@ func (daemon *Daemon) containerStatPath(container *container.Container, path str defer daemon.Unmount(container) err = daemon.mountVolumes(container) - defer container.UnmountVolumes(true) + defer container.UnmountVolumes(true, daemon.LogVolumeEvent) if err != nil { return nil, err } @@ -119,7 +119,7 @@ func (daemon *Daemon) containerArchivePath(container *container.Container, path defer func() { if err != nil { // unmount any volumes - container.UnmountVolumes(true) + container.UnmountVolumes(true, daemon.LogVolumeEvent) // unmount the container's rootfs daemon.Unmount(container) } @@ -154,7 +154,7 @@ func (daemon *Daemon) containerArchivePath(container *container.Container, path content = ioutils.NewReadCloserWrapper(data, func() error { err := data.Close() - container.UnmountVolumes(true) + container.UnmountVolumes(true, daemon.LogVolumeEvent) daemon.Unmount(container) container.Unlock() return err @@ -181,7 +181,7 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path defer daemon.Unmount(container) err = daemon.mountVolumes(container) - defer container.UnmountVolumes(true) + defer container.UnmountVolumes(true, daemon.LogVolumeEvent) if err != nil { return err } @@ -283,7 +283,7 @@ func (daemon *Daemon) containerCopy(container *container.Container, resource str defer func() { if err != nil { // unmount any volumes - container.UnmountVolumes(true) + container.UnmountVolumes(true, daemon.LogVolumeEvent) // unmount the container's rootfs daemon.Unmount(container) } @@ -320,7 +320,7 @@ func (daemon *Daemon) containerCopy(container *container.Container, resource str reader := ioutils.NewReadCloserWrapper(archive, func() error { err := archive.Close() - container.UnmountVolumes(true) + container.UnmountVolumes(true, daemon.LogVolumeEvent) daemon.Unmount(container) container.Unlock() return err diff --git a/daemon/container_operations_unix.go b/daemon/container_operations_unix.go index 5858f74fcf..f896912f9e 100644 --- a/daemon/container_operations_unix.go +++ b/daemon/container_operations_unix.go @@ -699,6 +699,7 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName return derr.ErrorCodeJoinInfo.WithArgs(err) } + daemon.LogNetworkEventWithAttributes(n, "connect", map[string]string{"container": container.ID}) return nil } @@ -719,6 +720,11 @@ func (daemon *Daemon) DisconnectFromNetwork(container *container.Container, n li if err := container.ToDiskLocking(); err != nil { return fmt.Errorf("Error saving container to disk: %v", err) } + + attributes := map[string]string{ + "container": container.ID, + } + daemon.LogNetworkEventWithAttributes(n, "disconnect", attributes) return nil } @@ -844,14 +850,18 @@ func (daemon *Daemon) releaseNetwork(container *container.Container) { } 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 } @@ -864,6 +874,13 @@ func (daemon *Daemon) releaseNetwork(container *container.Container) { if err := sb.Delete(); err != nil { 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 { diff --git a/daemon/create.go b/daemon/create.go index bebf1ac29e..42002ce98e 100644 --- a/daemon/create.go +++ b/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) { 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 } diff --git a/daemon/daemon.go b/daemon/daemon.go index e8189f1732..452c359525 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -22,6 +22,7 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" + eventtypes "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/strslice" @@ -47,7 +48,6 @@ import ( "github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/graphdb" "github.com/docker/docker/pkg/idtools" - "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/namesgenerator" "github.com/docker/docker/pkg/progress" @@ -554,23 +554,9 @@ func (daemon *Daemon) GetByName(name string) (*container.Container, error) { return e, nil } -// getEventFilter returns a filters.Filter for a set of filters -func (daemon *Daemon) getEventFilter(filter filters.Args) *events.Filter { - // incoming container filter can be name, id or partial id, convert to - // a full container id - for _, cn := range filter.Get("container") { - c, err := daemon.GetContainer(cn) - filter.Del("container", cn) - if err == nil { - filter.Add("container", c.ID) - } - } - return events.NewFilter(filter, daemon.GetLabels) -} - // SubscribeToEvents returns the currently record of events, a channel to stream new events from, and a function to cancel the stream of events. -func (daemon *Daemon) SubscribeToEvents(since, sinceNano int64, filter filters.Args) ([]*jsonmessage.JSONMessage, chan interface{}) { - ef := daemon.getEventFilter(filter) +func (daemon *Daemon) SubscribeToEvents(since, sinceNano int64, filter filters.Args) ([]eventtypes.Message, chan interface{}) { + ef := events.NewFilter(filter) return daemon.EventsService.SubscribeTopic(since, sinceNano, ef) } @@ -580,21 +566,6 @@ func (daemon *Daemon) UnsubscribeFromEvents(listener chan interface{}) { daemon.EventsService.Evict(listener) } -// GetLabels for a container or image id -func (daemon *Daemon) GetLabels(id string) map[string]string { - // TODO: TestCase - container := daemon.containers.Get(id) - if container != nil { - return container.Config.Labels - } - - img, err := daemon.GetImage(id) - if err == nil { - return img.ContainerConfig.Labels - } - return nil -} - // children returns all child containers of the container with the // given name. The containers are returned as a map from the container // name to a pointer to Container. @@ -1032,7 +1003,8 @@ func (daemon *Daemon) TagImage(newTag reference.Named, imageName string) error { if err := daemon.referenceStore.AddTag(newTag, imageID, true); err != nil { return err } - daemon.EventsService.Log("tag", newTag.String(), "") + + daemon.LogImageEvent(imageID.String(), newTag.String(), "tag") return nil } @@ -1068,15 +1040,15 @@ func (daemon *Daemon) PullImage(ref reference.Named, metaHeaders map[string][]st }() imagePullConfig := &distribution.ImagePullConfig{ - MetaHeaders: metaHeaders, - AuthConfig: authConfig, - ProgressOutput: progress.ChanOutput(progressChan), - RegistryService: daemon.RegistryService, - EventsService: daemon.EventsService, - MetadataStore: daemon.distributionMetadataStore, - ImageStore: daemon.imageStore, - ReferenceStore: daemon.referenceStore, - DownloadManager: daemon.downloadManager, + MetaHeaders: metaHeaders, + AuthConfig: authConfig, + ProgressOutput: progress.ChanOutput(progressChan), + RegistryService: daemon.RegistryService, + ImageEventLogger: daemon.LogImageEvent, + MetadataStore: daemon.distributionMetadataStore, + ImageStore: daemon.imageStore, + ReferenceStore: daemon.referenceStore, + DownloadManager: daemon.downloadManager, } err := distribution.Pull(ctx, ref, imagePullConfig) @@ -1111,17 +1083,17 @@ func (daemon *Daemon) PushImage(ref reference.Named, metaHeaders map[string][]st }() imagePushConfig := &distribution.ImagePushConfig{ - MetaHeaders: metaHeaders, - AuthConfig: authConfig, - ProgressOutput: progress.ChanOutput(progressChan), - RegistryService: daemon.RegistryService, - EventsService: daemon.EventsService, - MetadataStore: daemon.distributionMetadataStore, - LayerStore: daemon.layerStore, - ImageStore: daemon.imageStore, - ReferenceStore: daemon.referenceStore, - TrustKey: daemon.trustKey, - UploadManager: daemon.uploadManager, + MetaHeaders: metaHeaders, + AuthConfig: authConfig, + ProgressOutput: progress.ChanOutput(progressChan), + RegistryService: daemon.RegistryService, + ImageEventLogger: daemon.LogImageEvent, + MetadataStore: daemon.distributionMetadataStore, + LayerStore: daemon.layerStore, + ImageStore: daemon.imageStore, + ReferenceStore: daemon.referenceStore, + TrustKey: daemon.trustKey, + UploadManager: daemon.uploadManager, } err := distribution.Push(ctx, ref, imagePushConfig) diff --git a/daemon/delete.go b/daemon/delete.go index dc01433291..6aaecc1289 100644 --- a/daemon/delete.go +++ b/daemon/delete.go @@ -157,5 +157,6 @@ func (daemon *Daemon) VolumeRm(name string) error { } return derr.ErrorCodeRmVolume.WithArgs(name, err) } + daemon.LogVolumeEvent(v.Name(), "destroy", map[string]string{"driver": v.DriverName()}) return nil } diff --git a/daemon/events.go b/daemon/events.go index 969e1d3964..3211679c10 100644 --- a/daemon/events.go +++ b/daemon/events.go @@ -1,14 +1,81 @@ package daemon import ( + "strings" + + "github.com/docker/docker/api/types/events" "github.com/docker/docker/container" + "github.com/docker/libnetwork" ) // LogContainerEvent generates an event related to a container. func (daemon *Daemon) LogContainerEvent(container *container.Container, action string) { - daemon.EventsService.Log( - action, - container.ID, - container.Config.Image, - ) + attributes := copyAttributes(container.Config.Labels) + if container.Config.Image != "" { + attributes["image"] = container.Config.Image + } + attributes["name"] = strings.TrimLeft(container.Name, "/") + + actor := events.Actor{ + ID: container.ID, + Attributes: attributes, + } + daemon.EventsService.Log(action, events.ContainerEventType, actor) +} + +// LogImageEvent generates an event related to a container. +func (daemon *Daemon) LogImageEvent(imageID, refName, action string) { + attributes := map[string]string{} + img, err := daemon.GetImage(imageID) + if err == nil && img.Config != nil { + // image has not been removed yet. + // it could be missing if the event is `delete`. + attributes = copyAttributes(img.Config.Labels) + } + if refName != "" { + attributes["name"] = refName + } + actor := events.Actor{ + ID: imageID, + Attributes: attributes, + } + + daemon.EventsService.Log(action, events.ImageEventType, actor) +} + +// LogVolumeEvent generates an event related to a volume. +func (daemon *Daemon) LogVolumeEvent(volumeID, action string, attributes map[string]string) { + actor := events.Actor{ + ID: volumeID, + Attributes: attributes, + } + daemon.EventsService.Log(action, events.VolumeEventType, actor) +} + +// LogNetworkEvent generates an event related to a network with only the default attributes. +func (daemon *Daemon) LogNetworkEvent(nw libnetwork.Network, action string) { + daemon.LogNetworkEventWithAttributes(nw, action, map[string]string{}) +} + +// LogNetworkEventWithAttributes generates an event related to a network with specific given attributes. +func (daemon *Daemon) LogNetworkEventWithAttributes(nw libnetwork.Network, action string, attributes map[string]string) { + attributes["name"] = nw.Name() + attributes["type"] = nw.Type() + actor := events.Actor{ + ID: nw.ID(), + Attributes: attributes, + } + daemon.EventsService.Log(action, events.NetworkEventType, actor) +} + +// copyAttributes guarantees that labels are not mutated by event triggers. +func copyAttributes(labels map[string]string) map[string]string { + attributes := map[string]string{} + if labels == nil { + return attributes + } + for k, v := range labels { + attributes[k] = v + } + return attributes } diff --git a/daemon/events/events.go b/daemon/events/events.go index 3674170fe3..6f44e7c500 100644 --- a/daemon/events/events.go +++ b/daemon/events/events.go @@ -4,7 +4,7 @@ import ( "sync" "time" - "github.com/docker/docker/pkg/jsonmessage" + eventtypes "github.com/docker/docker/api/types/events" "github.com/docker/docker/pkg/pubsub" ) @@ -13,17 +13,17 @@ const ( bufferSize = 1024 ) -// Events is pubsub channel for *jsonmessage.JSONMessage +// Events is pubsub channel for events generated by the engine. type Events struct { mu sync.Mutex - events []*jsonmessage.JSONMessage + events []eventtypes.Message pub *pubsub.Publisher } // New returns new *Events instance func New() *Events { return &Events{ - events: make([]*jsonmessage.JSONMessage, 0, eventsLimit), + events: make([]eventtypes.Message, 0, eventsLimit), pub: pubsub.NewPublisher(100*time.Millisecond, bufferSize), } } @@ -32,9 +32,9 @@ func New() *Events { // last events, a channel in which you can expect new events (in form // of interface{}, so you need type assertion), and a function to call // to stop the stream of events. -func (e *Events) Subscribe() ([]*jsonmessage.JSONMessage, chan interface{}, func()) { +func (e *Events) Subscribe() ([]eventtypes.Message, chan interface{}, func()) { e.mu.Lock() - current := make([]*jsonmessage.JSONMessage, len(e.events)) + current := make([]eventtypes.Message, len(e.events)) copy(current, e.events) l := e.pub.Subscribe() e.mu.Unlock() @@ -48,13 +48,13 @@ func (e *Events) Subscribe() ([]*jsonmessage.JSONMessage, chan interface{}, func // SubscribeTopic adds new listener to events, returns slice of 64 stored // last events, a channel in which you can expect new events (in form // of interface{}, so you need type assertion). -func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]*jsonmessage.JSONMessage, chan interface{}) { +func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]eventtypes.Message, chan interface{}) { e.mu.Lock() defer e.mu.Unlock() - var buffered []*jsonmessage.JSONMessage + var buffered []eventtypes.Message topic := func(m interface{}) bool { - return ef.Include(m.(*jsonmessage.JSONMessage)) + return ef.Include(m.(eventtypes.Message)) } if since != -1 { @@ -64,7 +64,7 @@ func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]*jsonmess break } if ef.filter.Len() == 0 || topic(ev) { - buffered = append([]*jsonmessage.JSONMessage{ev}, buffered...) + buffered = append([]eventtypes.Message{ev}, buffered...) } } } @@ -87,9 +87,27 @@ func (e *Events) Evict(l chan interface{}) { // Log broadcasts event to listeners. Each listener has 100 millisecond for // receiving event or it will be skipped. -func (e *Events) Log(action, id, from string) { +func (e *Events) Log(action, eventType string, actor eventtypes.Actor) { now := time.Now().UTC() - jm := &jsonmessage.JSONMessage{Status: action, ID: id, From: from, Time: now.Unix(), TimeNano: now.UnixNano()} + jm := eventtypes.Message{ + Action: action, + Type: eventType, + Actor: actor, + Time: now.Unix(), + TimeNano: now.UnixNano(), + } + + // fill deprecated fields for container and images + switch eventType { + case eventtypes.ContainerEventType: + jm.ID = actor.ID + jm.Status = action + jm.From = actor.Attributes["image"] + case eventtypes.ImageEventType: + jm.ID = actor.ID + jm.Status = action + } + e.mu.Lock() if len(e.events) == cap(e.events) { // discard oldest event diff --git a/daemon/events/events_test.go b/daemon/events/events_test.go index cf3c28aac5..a782636370 100644 --- a/daemon/events/events_test.go +++ b/daemon/events/events_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/api/types/events" ) func TestEventsLog(t *testing.T) { @@ -18,10 +18,14 @@ func TestEventsLog(t *testing.T) { if count != 2 { t.Fatalf("Must be 2 subscribers, got %d", count) } - e.Log("test", "cont", "image") + actor := events.Actor{ + ID: "cont", + Attributes: map[string]string{"image": "image"}, + } + e.Log("test", events.ContainerEventType, actor) select { case msg := <-l1: - jmsg, ok := msg.(*jsonmessage.JSONMessage) + jmsg, ok := msg.(events.Message) if !ok { t.Fatalf("Unexpected type %T", msg) } @@ -42,7 +46,7 @@ func TestEventsLog(t *testing.T) { } select { case msg := <-l2: - jmsg, ok := msg.(*jsonmessage.JSONMessage) + jmsg, ok := msg.(events.Message) if !ok { t.Fatalf("Unexpected type %T", msg) } @@ -70,7 +74,10 @@ func TestEventsLogTimeout(t *testing.T) { c := make(chan struct{}) go func() { - e.Log("test", "cont", "image") + actor := events.Actor{ + ID: "image", + } + e.Log("test", events.ImageEventType, actor) close(c) }() @@ -88,7 +95,12 @@ func TestLogEvents(t *testing.T) { action := fmt.Sprintf("action_%d", i) id := fmt.Sprintf("cont_%d", i) from := fmt.Sprintf("image_%d", i) - e.Log(action, id, from) + + actor := events.Actor{ + ID: id, + Attributes: map[string]string{"image": from}, + } + e.Log(action, events.ContainerEventType, actor) } time.Sleep(50 * time.Millisecond) current, l, _ := e.Subscribe() @@ -97,16 +109,21 @@ func TestLogEvents(t *testing.T) { action := fmt.Sprintf("action_%d", num) id := fmt.Sprintf("cont_%d", num) from := fmt.Sprintf("image_%d", num) - e.Log(action, id, from) + + actor := events.Actor{ + ID: id, + Attributes: map[string]string{"image": from}, + } + e.Log(action, events.ContainerEventType, actor) } if len(e.events) != eventsLimit { t.Fatalf("Must be %d events, got %d", eventsLimit, len(e.events)) } - var msgs []*jsonmessage.JSONMessage + var msgs []events.Message for len(msgs) < 10 { m := <-l - jm, ok := (m).(*jsonmessage.JSONMessage) + jm, ok := (m).(events.Message) if !ok { t.Fatalf("Unexpected type %T", m) } diff --git a/daemon/events/filter.go b/daemon/events/filter.go index 084346cfff..fc4fa7aae1 100644 --- a/daemon/events/filter.go +++ b/daemon/events/filter.go @@ -1,46 +1,76 @@ package events import ( + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" ) // Filter can filter out docker events from a stream type Filter struct { - filter filters.Args - getLabels func(id string) map[string]string + filter filters.Args } // NewFilter creates a new Filter -func NewFilter(filter filters.Args, getLabels func(id string) map[string]string) *Filter { - return &Filter{filter: filter, getLabels: getLabels} +func NewFilter(filter filters.Args) *Filter { + return &Filter{filter: filter} } // Include returns true when the event ev is included by the filters -func (ef *Filter) Include(ev *jsonmessage.JSONMessage) bool { - return ef.filter.ExactMatch("event", ev.Status) && - ef.filter.ExactMatch("container", ev.ID) && - ef.isImageIncluded(ev.ID, ev.From) && - ef.isLabelFieldIncluded(ev.ID) +func (ef *Filter) Include(ev events.Message) bool { + 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") { 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 // 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 { diff --git a/daemon/events_test.go b/daemon/events_test.go new file mode 100644 index 0000000000..7035ebd06b --- /dev/null +++ b/daemon/events_test.go @@ -0,0 +1,36 @@ +package daemon + +import ( + "testing" + + containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/events" +) + +func TestLogContainerCopyLabels(t *testing.T) { + e := events.New() + _, l, _ := e.Subscribe() + defer e.Evict(l) + + container := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "container_id", + Name: "container_name", + Config: &containertypes.Config{ + Labels: map[string]string{ + "node": "1", + "os": "alpine", + }, + }, + }, + } + daemon := &Daemon{ + EventsService: e, + } + daemon.LogContainerEvent(container, "create") + + if _, mutated := container.Config.Labels["image"]; mutated { + t.Fatalf("Expected to not mutate the container labels, got %q", container.Config.Labels) + } +} diff --git a/daemon/image_delete.go b/daemon/image_delete.go index ab8217b5e1..b2aacfb606 100644 --- a/daemon/image_delete.go +++ b/daemon/image_delete.go @@ -87,7 +87,7 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} - daemon.EventsService.Log("untag", imgID.String(), "") + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") records = append(records, untaggedRecord) // If has remaining references then untag finishes the remove @@ -109,7 +109,7 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} - daemon.EventsService.Log("untag", imgID.String(), "") + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") records = append(records, untaggedRecord) } } @@ -174,7 +174,7 @@ func (daemon *Daemon) removeAllReferencesToImageID(imgID image.ID, records *[]ty untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} - daemon.EventsService.Log("untag", imgID.String(), "") + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") *records = append(*records, untaggedRecord) } @@ -243,7 +243,7 @@ func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDe return err } - daemon.EventsService.Log("delete", imgID.String(), "") + daemon.LogImageEvent(imgID.String(), imgID.String(), "delete") *records = append(*records, types.ImageDelete{Deleted: imgID.String()}) for _, removedLayer := range removedLayers { *records = append(*records, types.ImageDelete{Deleted: removedLayer.ChainID.String()}) diff --git a/daemon/import.go b/daemon/import.go index e74f2cf962..80d2146c23 100644 --- a/daemon/import.go +++ b/daemon/import.go @@ -97,7 +97,7 @@ func (daemon *Daemon) ImportImage(src string, newRef reference.Named, msg string } } - daemon.EventsService.Log("import", id.String(), "") + daemon.LogImageEvent(id.String(), id.String(), "import") outStream.Write(sf.FormatStatus("", id.String())) return nil } diff --git a/daemon/network.go b/daemon/network.go index e803ab1963..a3282517de 100644 --- a/daemon/network.go +++ b/daemon/network.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/docker/docker/api/types/network" + derr "github.com/docker/docker/errors" + "github.com/docker/docker/runconfig" "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.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) { @@ -178,3 +186,21 @@ func (daemon *Daemon) GetNetworkDriverList() map[string]bool { 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 +} diff --git a/daemon/start.go b/daemon/start.go index 8ea3f2db01..54cb94884f 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -156,7 +156,7 @@ func (daemon *Daemon) Cleanup(container *container.Container) { 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) } } diff --git a/daemon/update.go b/daemon/update.go index c1e55241aa..5699901e75 100644 --- a/daemon/update.go +++ b/daemon/update.go @@ -54,5 +54,7 @@ func (daemon *Daemon) update(name string, hostConfig *container.HostConfig) erro } } + daemon.LogContainerEvent(container, "update") + return nil } diff --git a/daemon/volumes_unix.go b/daemon/volumes_unix.go index f0d9f2df91..ea19953093 100644 --- a/daemon/volumes_unix.go +++ b/daemon/volumes_unix.go @@ -5,6 +5,7 @@ package daemon import ( "os" "sort" + "strconv" "github.com/docker/docker/container" "github.com/docker/docker/daemon/execdriver" @@ -30,6 +31,16 @@ func (daemon *Daemon) setupMounts(container *container.Container) ([]execdriver. Writable: m.RW, 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) } } diff --git a/distribution/pull.go b/distribution/pull.go index e367f2d272..cddc40e24e 100644 --- a/distribution/pull.go +++ b/distribution/pull.go @@ -8,7 +8,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api" "github.com/docker/docker/api/types" - "github.com/docker/docker/daemon/events" "github.com/docker/docker/distribution/metadata" "github.com/docker/docker/distribution/xfer" "github.com/docker/docker/image" @@ -32,8 +31,8 @@ type ImagePullConfig struct { // RegistryService is the registry service to use for TLS configuration // and endpoint lookup. RegistryService *registry.Service - // EventsService is the events service to use for logging. - EventsService *events.Events + // ImageEventLogger notifies events for a given image + ImageEventLogger func(id, name, action string) // MetadataStore is the storage backend for distribution-specific // metadata. MetadataStore metadata.Store @@ -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 } diff --git a/distribution/push.go b/distribution/push.go index fb401f4f21..9c49d40857 100644 --- a/distribution/push.go +++ b/distribution/push.go @@ -9,7 +9,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" "github.com/docker/docker/api/types" - "github.com/docker/docker/daemon/events" "github.com/docker/docker/distribution/metadata" "github.com/docker/docker/distribution/xfer" "github.com/docker/docker/image" @@ -35,8 +34,8 @@ type ImagePushConfig struct { // RegistryService is the registry service to use for TLS configuration // and endpoint lookup. RegistryService *registry.Service - // EventsService is the events service to use for logging. - EventsService *events.Events + // ImageEventLogger notifies events for a given image + ImageEventLogger func(id, name, action string) // MetadataStore is the storage backend for distribution-specific // metadata. MetadataStore metadata.Store @@ -156,7 +155,7 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo return err } - imagePushConfig.EventsService.Log("push", repoInfo.Name(), "") + imagePushConfig.ImageEventLogger(ref.String(), repoInfo.Name(), "push") return nil } diff --git a/docs/misc/deprecated.md b/docs/misc/deprecated.md index a5d62123ff..d455f1349e 100644 --- a/docs/misc/deprecated.md +++ b/docs/misc/deprecated.md @@ -12,6 +12,12 @@ parent = "mn_use_docker" 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` **Deprecated In Release: v1.10** diff --git a/docs/reference/api/docker_remote_api_v1.22.md b/docs/reference/api/docker_remote_api_v1.22.md index f519e20982..1b71fcb7f1 100644 --- a/docs/reference/api/docker_remote_api_v1.22.md +++ b/docs/reference/api/docker_remote_api_v1.22.md @@ -2274,17 +2274,24 @@ Status Codes: `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: - 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 +Docker volumes report the following events: + + create, mount, unmount, destroy + +Docker networks report the following events: + + create, connect, disconnect, destroy + **Example request**: GET /events?since=1374067924 @@ -2294,10 +2301,48 @@ and Docker images report: HTTP/1.1 200 OK 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: @@ -2308,6 +2353,9 @@ Query Parameters: - `event=`; -- event to filter - `image=`; -- image to filter - `label=`; -- image and container label to filter + - `type=`; -- either `container` or `image` or `volume` or `network` + - `volume=`; -- volume to filter + - `network=`; -- network to filter Status Codes: diff --git a/docs/reference/commandline/events.md b/docs/reference/commandline/events.md index 1af0e2337a..30eae105d9 100644 --- a/docs/reference/commandline/events.md +++ b/docs/reference/commandline/events.md @@ -19,14 +19,22 @@ parent = "smn_cli" --since="" Show all events created since 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 +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 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, @@ -57,9 +65,12 @@ container container 588a23dac085 *AND* the event type is *start* The currently supported filters are: * container (`container=`) -* event (`event=`) +* event (`event=`) * image (`image=`) * label (`label=` or `label==`) +* type (`type=`) +* volume (`volume=`) +* network (`network=`) ## Examples @@ -77,68 +88,78 @@ You'll need two shells for this example. **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:** $ 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' - 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' - 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, relative to the current time on the client machine: $ 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:** $ 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' - 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' - 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' - 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' - 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' - 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) diff --git a/errors/daemon.go b/errors/daemon.go index c32ae2926b..0a1bd7d477 100644 --- a/errors/daemon.go +++ b/errors/daemon.go @@ -939,4 +939,13 @@ var ( Description: "There was an error while trying to start a container", 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, + }) ) diff --git a/integration-cli/docker_api_events_test.go b/integration-cli/docker_api_events_test.go index d6f2609f89..5d6e817f02 100644 --- a/integration-cli/docker_api_events_test.go +++ b/integration-cli/docker_api_events_test.go @@ -1,10 +1,16 @@ package main import ( + "encoding/json" + "io" "net/http" + "net/url" + "strconv" + "strings" "time" "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/jsonmessage" "github.com/go-check/check" ) @@ -28,3 +34,40 @@ func (s *DockerSuite) TestEventsApiEmptyOutput(c *check.C) { c.Fatal("timeout waiting for events api to respond, should have responded immediately") } } + +func (s *DockerSuite) TestEventsApiBackwardsCompatible(c *check.C) { + since := daemonTime(c).Unix() + ts := strconv.FormatInt(since, 10) + + out, _ := dockerCmd(c, "run", "--name=foo", "-d", "busybox", "top") + containerID := strings.TrimSpace(out) + c.Assert(waitRun(containerID), checker.IsNil) + + q := url.Values{} + q.Set("since", ts) + + _, body, err := sockRequestRaw("GET", "/events?"+q.Encode(), nil, "") + c.Assert(err, checker.IsNil) + defer body.Close() + + dec := json.NewDecoder(body) + var containerCreateEvent *jsonmessage.JSONMessage + for { + var event jsonmessage.JSONMessage + if err := dec.Decode(&event); err != nil { + if err == io.EOF { + break + } + c.Fatal(err) + } + if event.Status == "create" && event.ID == containerID { + containerCreateEvent = &event + break + } + } + + c.Assert(containerCreateEvent, checker.Not(checker.IsNil)) + c.Assert(containerCreateEvent.Status, checker.Equals, "create") + c.Assert(containerCreateEvent.ID, checker.Equals, containerID) + c.Assert(containerCreateEvent.From, checker.Equals, "busybox") +} diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index b8972844d2..207082f201 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -2,7 +2,6 @@ package main import ( "archive/tar" - "bufio" "bytes" "encoding/json" "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) { testRequires(c, DaemonIsLinux) name := "testbuildrm" @@ -6489,33 +6390,26 @@ func (s *DockerSuite) TestBuildNoNamedVolume(c *check.C) { func (s *DockerSuite) TestBuildTagEvent(c *check.C) { testRequires(c, DaemonIsLinux) - observer, err := newEventObserver(c, "--filter", "event=tag") - c.Assert(err, check.IsNil) - err = observer.Start() - c.Assert(err, check.IsNil) - defer observer.Stop() + since := daemonTime(c).Unix() dockerFile := `FROM busybox RUN echo events ` - _, err = buildImage("test", dockerFile, false) + _, err := buildImage("test", dockerFile, false) c.Assert(err, check.IsNil) - matchTag := regexp.MustCompile("test:latest") - eventTag := make(chan bool) - matcher := func(text string) { - if matchTag.MatchString(text) { - close(eventTag) + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "type=image") + events := strings.Split(strings.TrimSpace(out), "\n") + actions := eventActionsByIDAndType(c, events, "test:latest", "image") + var foundTag bool + for _, a := range actions { + if a == "tag" { + foundTag = true + break } } - go observer.Match(matcher) - select { - case <-time.After(10 * time.Second): - c.Fatal(observer.TimeoutError("test:latest", "tag")) - case <-eventTag: - // We saw the tag event as expected. - } + c.Assert(foundTag, checker.True, check.Commentf("No tag event found:\n%s", out)) } // #15780 diff --git a/integration-cli/docker_cli_build_unix_test.go b/integration-cli/docker_cli_build_unix_test.go index 37b69ef3d2..7d204e1ebb 100644 --- a/integration-cli/docker_cli_build_unix_test.go +++ b/integration-cli/docker_cli_build_unix_test.go @@ -3,12 +3,16 @@ package main import ( + "bufio" + "bytes" "encoding/json" "io/ioutil" "os" "os/exec" "path/filepath" + "regexp" "strings" + "time" "github.com/docker/docker/pkg/integration/checker" "github.com/docker/go-units" @@ -115,5 +119,89 @@ func (s *DockerSuite) TestBuildAddChangeOwnership(c *check.C) { if _, err := buildImageFromContext(name, ctx, true); err != nil { 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 + } } diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go index c9647ec182..aaeb1b0931 100644 --- a/integration-cli/docker_cli_events_test.go +++ b/integration-cli/docker_cli_events_test.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "os/exec" - "regexp" "strconv" "strings" "sync" @@ -67,22 +66,31 @@ func (s *DockerSuite) TestEventsUntag(c *check.C) { } func (s *DockerSuite) TestEventsContainerFailStartDie(c *check.C) { - out, _ := dockerCmd(c, "images", "-q") image := strings.Split(out, "\n")[0] _, _, err := dockerCmdWithError("run", "--name", "testeventdie", image, "blerg") 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())) - events := strings.Split(out, "\n") - c.Assert(len(events), checker.GreaterThan, 1) //Missing expected event + events := strings.Split(strings.TrimSpace(out), "\n") - startEvent := strings.Fields(events[len(events)-3]) - dieEvent := strings.Fields(events[len(events)-2]) + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 1) //Missing expected event - 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)) + 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) { @@ -114,65 +122,44 @@ func (s *DockerSuite) TestEventsLimit(c *check.C) { func (s *DockerSuite) TestEventsContainerEvents(c *check.C) { testRequires(c, DaemonIsLinux) - dockerCmd(c, "run", "--rm", "busybox", "true") + containerID, _ := dockerCmd(c, "run", "--rm", "--name", "container-events-test", "busybox", "true") + containerID = strings.TrimSpace(containerID) + out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) events := strings.Split(out, "\n") events = events[:len(events)-1] - c.Assert(len(events), checker.GreaterOrEqualThan, 5) //Missing expected event - createEvent := strings.Fields(events[len(events)-5]) - attachEvent := strings.Fields(events[len(events)-4]) - startEvent := strings.Fields(events[len(events)-3]) - dieEvent := strings.Fields(events[len(events)-2]) - destroyEvent := strings.Fields(events[len(events)-1]) - c.Assert(createEvent[len(createEvent)-1], checker.Equals, "create", check.Commentf("event should be create, not %#v", createEvent)) - c.Assert(attachEvent[len(attachEvent)-1], checker.Equals, "attach", check.Commentf("event should be attach, not %#v", attachEvent)) - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(dieEvent[len(dieEvent)-1], checker.Equals, "die", check.Commentf("event should be die, not %#v", dieEvent)) - c.Assert(destroyEvent[len(destroyEvent)-1], checker.Equals, "destroy", check.Commentf("event should be destroy, not %#v", destroyEvent)) + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + containerEvents := eventActionsByIDAndType(c, events, "container-events-test", "container") + c.Assert(containerEvents, checker.HasLen, 5, check.Commentf("events: %v", events)) + + c.Assert(containerEvents[0], checker.Equals, "create", check.Commentf(out)) + c.Assert(containerEvents[1], checker.Equals, "attach", check.Commentf(out)) + c.Assert(containerEvents[2], checker.Equals, "start", check.Commentf(out)) + c.Assert(containerEvents[3], checker.Equals, "die", check.Commentf(out)) + c.Assert(containerEvents[4], checker.Equals, "destroy", check.Commentf(out)) } func (s *DockerSuite) TestEventsContainerEventsSinceUnixEpoch(c *check.C) { testRequires(c, DaemonIsLinux) - dockerCmd(c, "run", "--rm", "busybox", "true") + dockerCmd(c, "run", "--rm", "--name", "since-epoch-test", "busybox", "true") timeBeginning := time.Unix(0, 0).Format(time.RFC3339Nano) timeBeginning = strings.Replace(timeBeginning, "Z", ".000000000Z", -1) - out, _ := dockerCmd(c, "events", fmt.Sprintf("--since='%s'", timeBeginning), - fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since='%s'", timeBeginning), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) events := strings.Split(out, "\n") events = events[:len(events)-1] - c.Assert(len(events), checker.GreaterOrEqualThan, 5) //Missing expected event - createEvent := strings.Fields(events[len(events)-5]) - attachEvent := strings.Fields(events[len(events)-4]) - startEvent := strings.Fields(events[len(events)-3]) - dieEvent := strings.Fields(events[len(events)-2]) - destroyEvent := strings.Fields(events[len(events)-1]) - c.Assert(createEvent[len(createEvent)-1], checker.Equals, "create", check.Commentf("event should be create, not %#v", createEvent)) - c.Assert(attachEvent[len(attachEvent)-1], checker.Equals, "attach", check.Commentf("event should be attach, not %#v", attachEvent)) - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(dieEvent[len(dieEvent)-1], checker.Equals, "die", check.Commentf("event should be die, not %#v", dieEvent)) - c.Assert(destroyEvent[len(destroyEvent)-1], checker.Equals, "destroy", check.Commentf("event should be destroy, not %#v", destroyEvent)) -} + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + containerEvents := eventActionsByIDAndType(c, events, "since-epoch-test", "container") + c.Assert(containerEvents, checker.HasLen, 5, check.Commentf("events: %v", events)) -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)) + 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) { @@ -189,10 +176,10 @@ func (s *DockerSuite) TestEventsImageTag(c *check.C) { events := strings.Split(strings.TrimSpace(out), "\n") c.Assert(events, checker.HasLen, 1, check.Commentf("was expecting 1 event. out=%s", out)) event := strings.TrimSpace(events[0]) - expectedStr := image + ": tag" - - c.Assert(event, checker.HasSuffix, expectedStr, check.Commentf("wrong event format. expected='%s' got=%s", expectedStr, event)) + matches := parseEventText(event) + c.Assert(matchEventID(matches, image), checker.True, check.Commentf("matches: %v\nout:\n%s", matches, out)) + c.Assert(matches["action"], checker.Equals, "tag") } func (s *DockerSuite) TestEventsImagePull(c *check.C) { @@ -208,68 +195,45 @@ func (s *DockerSuite) TestEventsImagePull(c *check.C) { events := strings.Split(strings.TrimSpace(out), "\n") event := strings.TrimSpace(events[len(events)-1]) - - c.Assert(event, checker.HasSuffix, "hello-world:latest: pull", check.Commentf("Missing pull event - got:%q", event)) + matches := parseEventText(event) + c.Assert(matches["id"], checker.Equals, "hello-world:latest") + c.Assert(matches["action"], checker.Equals, "pull") } func (s *DockerSuite) TestEventsImageImport(c *check.C) { testRequires(c, DaemonIsLinux) - observer, err := newEventObserver(c) - c.Assert(err, checker.IsNil) - - err = observer.Start() - c.Assert(err, checker.IsNil) - defer observer.Stop() - out, _ := dockerCmd(c, "run", "-d", "busybox", "true") cleanedContainerID := strings.TrimSpace(out) - out, _, err = runCommandPipelineWithOutput( + since := daemonTime(c).Unix() + out, _, err := runCommandPipelineWithOutput( exec.Command(dockerBinary, "export", cleanedContainerID), exec.Command(dockerBinary, "import", "-"), ) c.Assert(err, checker.IsNil, check.Commentf("import failed with output: %q", out)) imageRef := strings.TrimSpace(out) - eventImport := make(chan bool) - matchImport := regexp.MustCompile(imageRef + `: import\z`) - matcher := func(text string) { - if matchImport.MatchString(text) { - close(eventImport) - } - } - go observer.Match(matcher) - - select { - case <-time.After(5 * time.Second): - c.Fatal(observer.TimeoutError(imageRef, "import")) - case <-eventImport: - // ignore, done - } + out, _ = dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=import") + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(events, checker.HasLen, 1) + matches := parseEventText(events[0]) + c.Assert(matches["id"], checker.Equals, imageRef, check.Commentf("matches: %v\nout:\n%s\n", matches, out)) + c.Assert(matches["action"], checker.Equals, "import", check.Commentf("matches: %v\nout:\n%s\n", matches, out)) } func (s *DockerSuite) TestEventsFilters(c *check.C) { testRequires(c, DaemonIsLinux) - parseEvents := func(out, match string) { - events := strings.Split(out, "\n") - events = events[:len(events)-1] - for _, event := range events { - eventFields := strings.Fields(event) - eventName := eventFields[len(eventFields)-1] - c.Assert(eventName, checker.Matches, match) - } - } since := daemonTime(c).Unix() dockerCmd(c, "run", "--rm", "busybox", "true") dockerCmd(c, "run", "--rm", "busybox", "true") out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=die") - parseEvents(out, "die") + parseEvents(c, out, "die") out, _ = dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=die", "--filter", "event=start") - parseEvents(out, "((die)|(start))") + parseEvents(c, out, "die|start") // make sure we at least got 2 start events count := strings.Count(out, "start") @@ -355,7 +319,8 @@ func (s *DockerSuite) TestEventsFilterImageLabels(c *check.C) { "events", fmt.Sprintf("--since=%d", since), 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") @@ -385,15 +350,9 @@ func (s *DockerSuite) TestEventsFilterContainer(c *check.C) { return fmt.Errorf("expected 4 events, got %v", events) } for _, event := range events { - e := strings.Fields(event) - if len(e) < 3 { - return fmt.Errorf("got malformed event: %s", event) - } - - // Check the id - parsedID := strings.TrimSuffix(e[1], ":") - if parsedID != id { - return fmt.Errorf("expected event for container id %s: %s - parsed container id: %s", id, event, parsedID) + matches := parseEventText(event) + if !matchEventID(matches, id) { + return fmt.Errorf("expected event for container id %s: %s - parsed container id: %s", id, event, matches["id"]) } } return nil @@ -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) { testRequires(c, DaemonIsLinux) since := daemonTime(c).Unix() @@ -490,7 +383,7 @@ func (s *DockerSuite) TestEventsCommit(c *check.C) { dockerCmd(c, "stop", cID) out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " commit\n", check.Commentf("Missing 'commit' log event")) + c.Assert(out, checker.Contains, "commit", check.Commentf("Missing 'commit' log event")) } func (s *DockerSuite) TestEventsCopy(c *check.C) { @@ -515,12 +408,12 @@ func (s *DockerSuite) TestEventsCopy(c *check.C) { dockerCmd(c, "cp", "cptest:/tmp/file", tempFile.Name()) out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " archive-path\n", check.Commentf("Missing 'archive-path' log event\n")) + c.Assert(out, checker.Contains, "archive-path", check.Commentf("Missing 'archive-path' log event\n")) dockerCmd(c, "cp", tempFile.Name(), "cptest:/tmp/filecopy") out, _ = dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " extract-to-dir\n", check.Commentf("Missing 'extract-to-dir' log event")) + c.Assert(out, checker.Contains, "extract-to-dir", check.Commentf("Missing 'extract-to-dir' log event")) } func (s *DockerSuite) TestEventsResize(c *check.C) { @@ -539,7 +432,7 @@ func (s *DockerSuite) TestEventsResize(c *check.C) { dockerCmd(c, "stop", cID) out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " resize\n", check.Commentf("Missing 'resize' log event")) + c.Assert(out, checker.Contains, "resize", check.Commentf("Missing 'resize' log event")) } func (s *DockerSuite) TestEventsAttach(c *check.C) { @@ -571,7 +464,7 @@ func (s *DockerSuite) TestEventsAttach(c *check.C) { dockerCmd(c, "stop", cID) out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " attach\n", check.Commentf("Missing 'attach' log event")) + c.Assert(out, checker.Contains, "attach", check.Commentf("Missing 'attach' log event")) } func (s *DockerSuite) TestEventsRename(c *check.C) { @@ -582,7 +475,7 @@ func (s *DockerSuite) TestEventsRename(c *check.C) { dockerCmd(c, "rename", "oldName", "newName") out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=newName", "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " rename\n", check.Commentf("Missing 'rename' log event\n")) + c.Assert(out, checker.Contains, "rename", check.Commentf("Missing 'rename' log event\n")) } func (s *DockerSuite) TestEventsTop(c *check.C) { @@ -597,7 +490,7 @@ func (s *DockerSuite) TestEventsTop(c *check.C) { dockerCmd(c, "stop", cID) out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, " top\n", check.Commentf("Missing 'top' log event")) + c.Assert(out, checker.Contains, " top", check.Commentf("Missing 'top' log event")) } // #13753 @@ -624,5 +517,71 @@ func (s *DockerRegistrySuite) TestEventsImageFilterPush(c *check.C) { dockerCmd(c, "push", repoName) out, _ = dockerCmd(c, "events", "--since=0", "-f", "image="+repoName, "-f", "event=push", "--until="+strconv.Itoa(int(since))) - c.Assert(out, checker.Contains, repoName+": push\n", check.Commentf("Missing 'push' log event")) + c.Assert(out, checker.Contains, repoName, check.Commentf("Missing 'push' log event for %s", repoName)) +} + +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)) } diff --git a/integration-cli/docker_cli_events_unix_test.go b/integration-cli/docker_cli_events_unix_test.go index c65394349e..7afc8fc105 100644 --- a/integration-cli/docker_cli_events_unix_test.go +++ b/integration-cli/docker_cli_events_unix_test.go @@ -65,18 +65,14 @@ func (s *DockerSuite) TestEventsOOMDisableFalse(c *check.C) { out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=oomFalse", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) events := strings.Split(strings.TrimSuffix(out, "\n"), "\n") - c.Assert(len(events), checker.GreaterOrEqualThan, 5) //Missing expected event + nEvents := len(events) - createEvent := strings.Fields(events[len(events)-5]) - attachEvent := strings.Fields(events[len(events)-4]) - startEvent := strings.Fields(events[len(events)-3]) - oomEvent := strings.Fields(events[len(events)-2]) - dieEvent := strings.Fields(events[len(events)-1]) - c.Assert(createEvent[len(createEvent)-1], checker.Equals, "create", check.Commentf("event should be create, not %#v", createEvent)) - c.Assert(attachEvent[len(attachEvent)-1], checker.Equals, "attach", check.Commentf("event should be attach, not %#v", attachEvent)) - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(oomEvent[len(oomEvent)-1], checker.Equals, "oom", check.Commentf("event should be oom, not %#v", oomEvent)) - c.Assert(dieEvent[len(dieEvent)-1], checker.Equals, "die", check.Commentf("event should be die, not %#v", dieEvent)) + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + c.Assert(parseEventAction(c, events[nEvents-5]), checker.Equals, "create") + c.Assert(parseEventAction(c, events[nEvents-4]), checker.Equals, "attach") + c.Assert(parseEventAction(c, events[nEvents-3]), checker.Equals, "start") + c.Assert(parseEventAction(c, events[nEvents-2]), checker.Equals, "oom") + c.Assert(parseEventAction(c, events[nEvents-1]), checker.Equals, "die") } func (s *DockerSuite) TestEventsOOMDisableTrue(c *check.C) { @@ -98,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())) events := strings.Split(strings.TrimSuffix(out, "\n"), "\n") - c.Assert(len(events), checker.GreaterOrEqualThan, 4) //Missing expected event + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 4) //Missing expected event - createEvent := strings.Fields(events[len(events)-4]) - attachEvent := strings.Fields(events[len(events)-3]) - startEvent := strings.Fields(events[len(events)-2]) - oomEvent := strings.Fields(events[len(events)-1]) - - c.Assert(createEvent[len(createEvent)-1], checker.Equals, "create", check.Commentf("event should be create, not %#v", createEvent)) - c.Assert(attachEvent[len(attachEvent)-1], checker.Equals, "attach", check.Commentf("event should be attach, not %#v", attachEvent)) - c.Assert(startEvent[len(startEvent)-1], checker.Equals, "start", check.Commentf("event should be start, not %#v", startEvent)) - c.Assert(oomEvent[len(oomEvent)-1], checker.Equals, "oom", check.Commentf("event should be oom, not %#v", oomEvent)) + c.Assert(parseEventAction(c, events[nEvents-4]), checker.Equals, "create") + c.Assert(parseEventAction(c, events[nEvents-3]), checker.Equals, "attach") + c.Assert(parseEventAction(c, events[nEvents-2]), checker.Equals, "start") + c.Assert(parseEventAction(c, events[nEvents-1]), checker.Equals, "oom") out, _ = dockerCmd(c, "inspect", "-f", "{{.State.Status}}", "oomTrue") c.Assert(strings.TrimSpace(out), checker.Equals, "running", check.Commentf("container should be still running")) } } + +// #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") +} diff --git a/integration-cli/docker_cli_pause_test.go b/integration-cli/docker_cli_pause_test.go index afe62c60db..a88d32a0e1 100644 --- a/integration-cli/docker_cli_pause_test.go +++ b/integration-cli/docker_cli_pause_test.go @@ -23,15 +23,11 @@ func (s *DockerSuite) TestPause(c *check.C) { dockerCmd(c, "unpause", name) out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) - events := strings.Split(out, "\n") - c.Assert(len(events) > 1, checker.Equals, true) - - pauseEvent := strings.Fields(events[len(events)-3]) - unpauseEvent := strings.Fields(events[len(events)-2]) - - c.Assert(pauseEvent[len(pauseEvent)-1], checker.Equals, "pause") - c.Assert(unpauseEvent[len(unpauseEvent)-1], checker.Equals, "unpause") + events := strings.Split(strings.TrimSpace(out), "\n") + actions := eventActionsByIDAndType(c, events, name, "container") + c.Assert(actions[len(actions)-2], checker.Equals, "pause") + c.Assert(actions[len(actions)-1], checker.Equals, "unpause") } func (s *DockerSuite) TestPauseMultipleContainers(c *check.C) { @@ -53,21 +49,12 @@ func (s *DockerSuite) TestPauseMultipleContainers(c *check.C) { dockerCmd(c, append([]string{"unpause"}, containers...)...) out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) - events := strings.Split(out, "\n") - c.Assert(len(events) > len(containers)*3-2, checker.Equals, true) + events := strings.Split(strings.TrimSpace(out), "\n") - pauseEvents := make([][]string, len(containers)) - unpauseEvents := make([][]string, len(containers)) - for i := range containers { - pauseEvents[i] = strings.Fields(events[len(events)-len(containers)*2-1+i]) - unpauseEvents[i] = strings.Fields(events[len(events)-len(containers)-1+i]) - } + for _, name := range containers { + actions := eventActionsByIDAndType(c, events, name, "container") - for _, pauseEvent := range pauseEvents { - c.Assert(pauseEvent[len(pauseEvent)-1], checker.Equals, "pause") + c.Assert(actions[len(actions)-2], checker.Equals, "pause") + c.Assert(actions[len(actions)-1], checker.Equals, "unpause") } - for _, unpauseEvent := range unpauseEvents { - c.Assert(unpauseEvent[len(unpauseEvent)-1], checker.Equals, "unpause") - } - } diff --git a/integration-cli/events_utils.go b/integration-cli/events_utils.go index faff4fc6f8..8397a57f07 100644 --- a/integration-cli/events_utils.go +++ b/integration-cli/events_utils.go @@ -6,27 +6,50 @@ import ( "fmt" "io" "os/exec" + "regexp" "strconv" + "strings" + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/integration/checker" "github.com/go-check/check" ) +var ( + reTimestamp = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{9}(:?(:?(:?-|\+)\d{2}:\d{2})|Z)` + reEventType = `(?P\w+)` + reAction = `(?P\w+)` + reID = `(?P[^\s]+)` + reAttributes = `(\s\((?P[^\)]+)\))?` + reString = fmt.Sprintf(`\A%s\s%s\s%s\s%s%s\z`, reTimestamp, reEventType, reAction, reID, reAttributes) + + // eventCliRegexp is a regular expression that matches all possible event outputs in the cli + eventCliRegexp = regexp.MustCompile(reString) +) + // eventMatcher is a function that tries to match an event input. -type eventMatcher func(text string) +type eventMatcher func(text string) bool // eventObserver runs an events commands and observes its output. 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 // without running it. Users must call `eventObserver.Start` to start the command. func newEventObserver(c *check.C, args ...string) (*eventObserver, error) { since := daemonTime(c).Unix() + return newEventObserverWithBacklog(c, since, args...) +} - 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 { cmdArgs = append(cmdArgs, args...) } @@ -37,9 +60,10 @@ func newEventObserver(c *check.C, args ...string) (*eventObserver, error) { } return &eventObserver{ - buffer: new(bytes.Buffer), - command: eventsCmd, - stdout: stdout, + buffer: new(bytes.Buffer), + command: eventsCmd, + scanner: bufio.NewScanner(stdout), + startTime: startTime, }, nil } @@ -51,28 +75,144 @@ func (e *eventObserver) Start() error { // Stop stops the events command. func (e *eventObserver) Stop() { e.command.Process.Kill() + e.command.Process.Release() } // Match tries to match the events output with a given matcher. 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("\n") match(text) } + + err := e.scanner.Err() + if err == nil { + err = io.EOF + } + + logrus.Debug("EventObserver scanner loop finished: %v", err) + e.disconnectionError = err } -// 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()) +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) + } } -// output returns the events command output read until now by the Match goroutine. -func (e *eventObserver) output() string { - return e.buffer.String() +// matchEventLine matches a text with the event regular expression. +// It returns the action and true if the regular expression matches with the given id and event type. +// It returns an empty string and false if there is no match. +func matchEventLine(id, eventType string, actions map[string]chan bool) eventMatcher { + return func(text string) 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 + } +} + +// 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"])) + } }