Add event logging to libpod, even display to podman

In lipod, we now log major events that occurr.  These events
can be displayed using the `podman events` command. Each
event contains:

* Type (container, image, volume, pod...)
* Status (create, rm, stop, kill, ....)
* Timestamp in RFC3339Nano format
* Name (if applicable)
* Image (if applicable)

The format of the event and the varlink endpoint are to not
be considered stable until cockpit has done its enablement.

Signed-off-by: baude <bbaude@redhat.com>
This commit is contained in:
baude 2019-02-28 14:15:56 -06:00
parent 6421208e0f
commit ca1e76ff63
33 changed files with 1173 additions and 64 deletions

47
API.md
View File

@ -45,6 +45,8 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[func GetContainersByContext(all: bool, latest: bool, args: []string) []string](#GetContainersByContext) [func GetContainersByContext(all: bool, latest: bool, args: []string) []string](#GetContainersByContext)
[func GetEvents(options: EventInput) Event](#GetEvents)
[func GetImage(id: string) Image](#GetImage) [func GetImage(id: string) Image](#GetImage)
[func GetInfo() PodmanInfo](#GetInfo) [func GetInfo() PodmanInfo](#GetInfo)
@ -165,6 +167,10 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[type CreateResourceConfig](#CreateResourceConfig) [type CreateResourceConfig](#CreateResourceConfig)
[type Event](#Event)
[type EventInput](#EventInput)
[type IDMap](#IDMap) [type IDMap](#IDMap)
[type IDMappingOptions](#IDMappingOptions) [type IDMappingOptions](#IDMappingOptions)
@ -231,8 +237,12 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[error RuntimeError](#RuntimeError) [error RuntimeError](#RuntimeError)
[error StreamEnded](#StreamEnded)
[error VolumeNotFound](#VolumeNotFound) [error VolumeNotFound](#VolumeNotFound)
[error WantsMoreRequired](#WantsMoreRequired)
## Methods ## Methods
### <a name="BuildImage"></a>func BuildImage ### <a name="BuildImage"></a>func BuildImage
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;"> <div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
@ -469,6 +479,11 @@ method GetContainersByContext(all: [bool](https://godoc.org/builtin#bool), lates
GetContainersByContext allows you to get a list of container ids depending on all, latest, or a list of GetContainersByContext allows you to get a list of container ids depending on all, latest, or a list of
container names. The definition of latest container means the latest by creation date. In a multi- container names. The definition of latest container means the latest by creation date. In a multi-
user environment, results might differ from what you expect. user environment, results might differ from what you expect.
### <a name="GetEvents"></a>func GetEvents
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
method GetEvents(options: [EventInput](#EventInput)) [Event](#Event)</div>
GetEvents returns known libpod events filtered by the options provided.
### <a name="GetImage"></a>func GetImage ### <a name="GetImage"></a>func GetImage
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;"> <div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
@ -1396,6 +1411,32 @@ pids_limit [int](https://godoc.org/builtin#int)
shm_size [int](https://godoc.org/builtin#int) shm_size [int](https://godoc.org/builtin#int)
ulimit [[]string](#[]string) ulimit [[]string](#[]string)
### <a name="Event"></a>type Event
Event describes a libpod struct
id [string](https://godoc.org/builtin#string)
image [string](https://godoc.org/builtin#string)
name [string](https://godoc.org/builtin#string)
status [string](https://godoc.org/builtin#string)
time [string](https://godoc.org/builtin#string)
type [string](https://godoc.org/builtin#string)
### <a name="EventInput"></a>type EventInput
EventInput describes the input to obtain libpod events
filter [[]string](#[]string)
since [string](https://godoc.org/builtin#string)
stream [bool](https://godoc.org/builtin#bool)
until [string](https://godoc.org/builtin#string)
### <a name="IDMap"></a>type IDMap ### <a name="IDMap"></a>type IDMap
IDMap is used to describe user name spaces during container creation IDMap is used to describe user name spaces during container creation
@ -1752,6 +1793,12 @@ PodNotFound means the pod could not be found by the provided name or ID in local
### <a name="RuntimeError"></a>type RuntimeError ### <a name="RuntimeError"></a>type RuntimeError
RuntimeErrors generally means a runtime could not be found or gotten. RuntimeErrors generally means a runtime could not be found or gotten.
### <a name="StreamEnded"></a>type StreamEnded
The Podman endpoint has closed because the stream ended.
### <a name="VolumeNotFound"></a>type VolumeNotFound ### <a name="VolumeNotFound"></a>type VolumeNotFound
VolumeNotFound means the volume could not be found by the name or ID in local storage. VolumeNotFound means the volume could not be found by the name or ID in local storage.
### <a name="WantsMoreRequired"></a>type WantsMoreRequired
The Podman endpoint requires that you use a streaming connection.

View File

@ -53,6 +53,15 @@ type ImagesValues struct {
Sort string Sort string
} }
type EventValues struct {
PodmanCommand
Filter []string
Format string
Since string
Stream bool
Until string
}
type TagValues struct { type TagValues struct {
PodmanCommand PodmanCommand
} }

48
cmd/podman/events.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/pkg/adapter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var (
eventsCommand cliconfig.EventValues
eventsDescription = "Monitor podman events"
_eventsCommand = &cobra.Command{
Use: "events [flags]",
Short: "show podman events",
Long: eventsDescription,
RunE: func(cmd *cobra.Command, args []string) error {
eventsCommand.InputArgs = args
eventsCommand.GlobalFlags = MainGlobalOpts
return eventsCmd(&eventsCommand)
},
Example: `podman events
podman events --filter event=create
podman events --since 1h30s`,
}
)
func init() {
eventsCommand.Command = _eventsCommand
eventsCommand.SetUsageTemplate(UsageTemplate())
flags := eventsCommand.Flags()
flags.StringArrayVar(&eventsCommand.Filter, "filter", []string{}, "filter output")
flags.StringVar(&eventsCommand.Format, "format", "", "format the output using a Go template")
flags.BoolVar(&eventsCommand.Stream, "stream", true, "stream new events; for testing only")
flags.StringVar(&eventsCommand.Since, "since", "", "show all events created since timestamp")
flags.StringVar(&eventsCommand.Until, "until", "", "show all events until timestamp")
flags.MarkHidden("stream")
}
func eventsCmd(c *cliconfig.EventValues) error {
runtime, err := adapter.GetRuntime(&c.PodmanCommand)
if err != nil {
return errors.Wrapf(err, "error creating libpod runtime")
}
defer runtime.Shutdown(false)
return runtime.Events(c)
}

View File

@ -8,6 +8,7 @@ import (
"github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/logs" "github.com/containers/libpod/pkg/logs"
"github.com/containers/libpod/pkg/util"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -70,7 +71,7 @@ func logsCmd(c *cliconfig.LogsValues) error {
sinceTime := time.Time{} sinceTime := time.Time{}
if c.Flag("since").Changed { if c.Flag("since").Changed {
// parse time, error out if something is wrong // parse time, error out if something is wrong
since, err := parseInputTime(c.Since) since, err := util.ParseInputTime(c.Since)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not parse time: %q", c.Since) return errors.Wrapf(err, "could not parse time: %q", c.Since)
} }
@ -112,25 +113,3 @@ func logsCmd(c *cliconfig.LogsValues) error {
} }
return logs.ReadLogs(logPath, ctr, opts) return logs.ReadLogs(logPath, ctr, opts)
} }
// parseInputTime takes the users input and to determine if it is valid and
// returns a time format and error. The input is compared to known time formats
// or a duration which implies no-duration
func parseInputTime(inputTime string) (time.Time, error) {
timeFormats := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05", "2006-01-02T15:04:05.999999999",
"2006-01-02Z07:00", "2006-01-02"}
// iterate the supported time formats
for _, tf := range timeFormats {
t, err := time.Parse(tf, inputTime)
if err == nil {
return t, nil
}
}
// input might be a duration
duration, err := time.ParseDuration(inputTime)
if err != nil {
return time.Time{}, errors.Errorf("unable to interpret time value")
}
return time.Now().Add(-duration), nil
}

View File

@ -36,6 +36,7 @@ var (
// implemented. // implemented.
var mainCommands = []*cobra.Command{ var mainCommands = []*cobra.Command{
_buildCommand, _buildCommand,
_eventsCommand,
_exportCommand, _exportCommand,
_historyCommand, _historyCommand,
&_imagesCommand, &_imagesCommand,

115
cmd/podman/shared/events.go Normal file
View File

@ -0,0 +1,115 @@
package shared
import (
"fmt"
"strings"
"time"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/util"
"github.com/pkg/errors"
)
func generateEventFilter(filter, filterValue string) (func(e *events.Event) bool, error) {
switch strings.ToUpper(filter) {
case "CONTAINER":
return func(e *events.Event) bool {
if e.Type != events.Container {
return false
}
if e.Name == filterValue {
return true
}
return strings.HasPrefix(e.ID, filterValue)
}, nil
case "EVENT", "STATUS":
return func(e *events.Event) bool {
return fmt.Sprintf("%s", e.Status) == filterValue
}, nil
case "IMAGE":
return func(e *events.Event) bool {
if e.Type != events.Image {
return false
}
if e.Name == filterValue {
return true
}
return strings.HasPrefix(e.ID, filterValue)
}, nil
case "POD":
return func(e *events.Event) bool {
if e.Type != events.Pod {
return false
}
if e.Name == filterValue {
return true
}
return strings.HasPrefix(e.ID, filterValue)
}, nil
case "VOLUME":
return func(e *events.Event) bool {
if e.Type != events.Volume {
return false
}
return strings.HasPrefix(e.ID, filterValue)
}, nil
case "TYPE":
return func(e *events.Event) bool {
return fmt.Sprintf("%s", e.Type) == filterValue
}, nil
}
return nil, errors.Errorf("%s is an invalid filter", filter)
}
func generateEventSinceOption(timeSince time.Time) func(e *events.Event) bool {
return func(e *events.Event) bool {
return e.Time.After(timeSince)
}
}
func generateEventUntilOption(timeUntil time.Time) func(e *events.Event) bool {
return func(e *events.Event) bool {
return e.Time.Before(timeUntil)
}
}
func parseFilter(filter string) (string, string, error) {
filterSplit := strings.Split(filter, "=")
if len(filterSplit) != 2 {
return "", "", errors.Errorf("%s is an invalid filter", filter)
}
return filterSplit[0], filterSplit[1], nil
}
func GenerateEventOptions(filters []string, since, until string) ([]events.EventFilter, error) {
var options []events.EventFilter
for _, filter := range filters {
key, val, err := parseFilter(filter)
if err != nil {
return nil, err
}
funcFilter, err := generateEventFilter(key, val)
if err != nil {
return nil, err
}
options = append(options, funcFilter)
}
if len(since) > 0 {
timeSince, err := util.ParseInputTime(since)
if err != nil {
return nil, errors.Wrapf(err, "unable to convert since time of %s", since)
}
options = append(options, generateEventSinceOption(timeSince))
}
if len(until) > 0 {
timeUntil, err := util.ParseInputTime(until)
if err != nil {
return nil, errors.Wrapf(err, "unable to convert until time of %s", until)
}
options = append(options, generateEventUntilOption(timeUntil))
}
return options, nil
}

View File

@ -435,6 +435,23 @@ type Runlabel(
opts: [string]string opts: [string]string
) )
# Event describes a libpod struct
type Event(
# TODO: make status and type a enum at some point?
# id is the container, volume, pod, image ID
id: string,
# image is the image name where applicable
image: string,
# name is the name of the pod, container, image
name: string,
# status describes the event that happened (i.e. create, remove, ...)
status: string,
# time the event happened
time: string,
# type describes object the event happened with (image, container...)
type: string
)
# GetVersion returns version and build information of the podman service # GetVersion returns version and build information of the podman service
method GetVersion() -> ( method GetVersion() -> (
version: string, version: string,
@ -1123,6 +1140,9 @@ method GetPodsByContext(all: bool, latest: bool, args: []string) -> (pods: []str
# LoadImage allows you to load an image into local storage from a tarball. # LoadImage allows you to load an image into local storage from a tarball.
method LoadImage(name: string, inputFile: string, quiet: bool, deleteFile: bool) -> (reply: MoreResponse) method LoadImage(name: string, inputFile: string, quiet: bool, deleteFile: bool) -> (reply: MoreResponse)
# GetEvents returns known libpod events filtered by the options provided.
method GetEvents(filter: []string, since: string, stream: bool, until: string) -> (events: Event)
# ImageNotFound means the image could not be found by the provided name or ID in local storage. # ImageNotFound means the image could not be found by the provided name or ID in local storage.
error ImageNotFound (id: string, reason: string) error ImageNotFound (id: string, reason: string)
@ -1152,3 +1172,6 @@ error ErrorOccurred (reason: string)
# RuntimeErrors generally means a runtime could not be found or gotten. # RuntimeErrors generally means a runtime could not be found or gotten.
error RuntimeError (reason: string) error RuntimeError (reason: string)
# The Podman endpoint requires that you use a streaming connection.
error WantsMoreRequired (reason: string)

View File

@ -19,6 +19,7 @@
| [podman-cp(1)](/docs/podman-cp.1.md) | Copy files/folders between a container and the local filesystem || | [podman-cp(1)](/docs/podman-cp.1.md) | Copy files/folders between a container and the local filesystem ||
| [podman-create(1)](/docs/podman-create.1.md) | Create a new container || | [podman-create(1)](/docs/podman-create.1.md) | Create a new container ||
| [podman-diff(1)](/docs/podman-diff.1.md) | Inspect changes on a container or image's filesystem |[![...](/docs/play.png)](https://asciinema.org/a/FXfWB9CKYFwYM4EfqW3NSZy1G)| | [podman-diff(1)](/docs/podman-diff.1.md) | Inspect changes on a container or image's filesystem |[![...](/docs/play.png)](https://asciinema.org/a/FXfWB9CKYFwYM4EfqW3NSZy1G)|
| [podman-events(1)](/docs/podman-events.1.md) | Monitor Podman events ||
| [podman-exec(1)](/docs/podman-exec.1.md) | Execute a command in a running container | [podman-exec(1)](/docs/podman-exec.1.md) | Execute a command in a running container
| [podman-export(1)](/docs/podman-export.1.md) | Export container's filesystem contents as a tar archive |[![...](/docs/play.png)](https://asciinema.org/a/913lBIRAg5hK8asyIhhkQVLtV)| | [podman-export(1)](/docs/podman-export.1.md) | Export container's filesystem contents as a tar archive |[![...](/docs/play.png)](https://asciinema.org/a/913lBIRAg5hK8asyIhhkQVLtV)|
| [podman-generate(1)](/docs/podman-generate.1.md) | Generate structured output based on Podman containers and pods | | | [podman-generate(1)](/docs/podman-generate.1.md) | Generate structured output based on Podman containers and pods | |

View File

@ -2438,6 +2438,22 @@ _podman_play_kube() {
esac esac
} }
_podman_events() {
local options_with_args="
--help
--h
--filter
--format
--since
--until
"
case "$cur" in
-*)
COMPREPLY=($(compgen -W "$options_with_args" -- "$cur"))
;;
esac
}
_podman_container_runlabel() { _podman_container_runlabel() {
local options_with_args=" local options_with_args="
--authfile --authfile
@ -3027,6 +3043,7 @@ _podman_podman() {
cp cp
create create
diff diff
events
exec exec
export export
generate generate

139
docs/podman-events.1.md Normal file
View File

@ -0,0 +1,139 @@
% podman-events(1)
## NAME
podman\-events- Monitor Podman events
## SYNOPSIS
**podman events** [*options*]
## DESCRIPTION
Monitor and print events that occur in Podman. Each event will include a timestamp,
a type, a status, name (if applicable), and image (if applicable).
The *container* event type will report the follow statuses:
* attach
* checkpoint
* cleanup
* commit
* create
* exec
* export
* import
* init
* kill
* mount
* pause
* prune
* remove
* restore
* start
* stop
* sync
* unmount
* unpause
* wait
The *pod* event type will report the follow statuses:
* create
* kill
* pause
* remove
* start
* stop
* unpause
The *image* event type will report the following statuses:
* prune
* pull
* push
* remove
* save
* tag
* untag
The *volume* type will report the following statuses:
* create
* prune
* remove
## OPTIONS
**--help**
Print usage statement.
**--format**
Format the output using the given Go template. An output value of *json* is not supported.
**--filter**=[]
Filter events that are displayed. They must be in the format of "filter=value". The following
filters are supported:
* container=name_or_id
* event=event_status (described above)
* image=name_or_id
* pod=name_or_id
* volume=name_or_id
* type=event_type (described above)
In the case where an ID is used, the ID may be in its full or shortened form.
**--since**=[]
Show all events created since the given timestamp
**--until**=[]
Show all events created until the given timestamp
The *since* and *until* values can be RFC3339Nano time stamps or a Go duration string such as 10m, 5h. If no
*since* or *until* values are provided, only new events will be shown.
## EXAMPLES
Showing podman events
```
$ podman events
2019-03-02 10:33:42.312377447 -0600 CST container create 34503c192940 (image=docker.io/library/alpine:latest, name=friendly_allen)
2019-03-02 10:33:46.958768077 -0600 CST container init 34503c192940 (image=docker.io/library/alpine:latest, name=friendly_allen)
2019-03-02 10:33:46.973661968 -0600 CST container start 34503c192940 (image=docker.io/library/alpine:latest, name=friendly_allen)
2019-03-02 10:33:50.833761479 -0600 CST container stop 34503c192940 (image=docker.io/library/alpine:latest, name=friendly_allen)
2019-03-02 10:33:51.047104966 -0600 CST container cleanup 34503c192940 (image=docker.io/library/alpine:latest, name=friendly_allen)
```
Show only podman create events
```
$ podman events --filter event=create
2019-03-02 10:36:01.375685062 -0600 CST container create 20dc581f6fbf (image=docker.io/library/alpine:latest, name=sharp_morse)
2019-03-02 10:36:08.561188337 -0600 CST container create 58e7e002344c (image=k8s.gcr.io/pause:3.1, name=3e701f270d54-infra)
2019-03-02 10:36:13.146899437 -0600 CST volume create cad6dc50e087 (image=, name=cad6dc50e0879568e7d656bd004bd343d6035e7fc4024e1711506fe2fd459e6f)
2019-03-02 10:36:29.978806894 -0600 CST container create d81e30f1310f (image=docker.io/library/busybox:latest, name=musing_newton)
```
Show only podman pod create events
```
$ podman events --filter event=create --filter type=pod
2019-03-02 10:44:29.601746633 -0600 CST pod create 1df5ebca7b44 (image=, name=confident_hawking)
2019-03-02 10:44:42.374637304 -0600 CST pod create ca731231718e (image=, name=webapp)
2019-03-02 10:44:47.486759133 -0600 CST pod create 71e807fc3a8e (image=, name=reverent_swanson)
```
Show only podman events created in the last five minutes:
```
$ sudo podman events --since 5m
2019-03-02 10:44:29.598835409 -0600 CST container create b629d10d3831 (image=k8s.gcr.io/pause:3.1, name=1df5ebca7b44-infra)
2019-03-02 10:44:29.601746633 -0600 CST pod create 1df5ebca7b44 (image=, name=confident_hawking)
2019-03-02 10:44:42.371100253 -0600 CST container create 170a0f457d00 (image=k8s.gcr.io/pause:3.1, name=ca731231718e-infra)
2019-03-02 10:44:42.374637304 -0600 CST pod create ca731231718e (image=, name=webapp)
```
## SEE ALSO
podman(1)
## HISTORY
March 2019, Originally compiled by Brent Baude <bbaude@redhat.com>

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/containers/libpod/libpod/driver" "github.com/containers/libpod/libpod/driver"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/inspect" "github.com/containers/libpod/pkg/inspect"
"github.com/containers/libpod/pkg/lookup" "github.com/containers/libpod/pkg/lookup"
"github.com/containers/storage/pkg/stringid" "github.com/containers/storage/pkg/stringid"
@ -88,6 +89,7 @@ func (c *Container) Start(ctx context.Context, recursive bool) (err error) {
} }
// Start the container // Start the container
defer c.newContainerEvent(events.Start)
return c.start() return c.start()
} }
@ -125,7 +127,8 @@ func (c *Container) StartAndAttach(ctx context.Context, streams *AttachStreams,
} }
close(attachChan) close(attachChan)
}() }()
c.newContainerEvent(events.Start)
c.newContainerEvent(events.Attach)
return attachChan, nil return attachChan, nil
} }
@ -180,7 +183,7 @@ func (c *Container) StopWithTimeout(timeout uint) error {
c.state.State == ContainerStateExited { c.state.State == ContainerStateExited {
return ErrCtrStopped return ErrCtrStopped
} }
defer c.newContainerEvent(events.Stop)
return c.stop(timeout) return c.stop(timeout)
} }
@ -198,7 +201,7 @@ func (c *Container) Kill(signal uint) error {
if c.state.State != ContainerStateRunning { if c.state.State != ContainerStateRunning {
return errors.Wrapf(ErrCtrStateInvalid, "can only kill running containers") return errors.Wrapf(ErrCtrStateInvalid, "can only kill running containers")
} }
defer c.newContainerEvent(events.Kill)
return c.runtime.ociRuntime.killContainer(c, signal) return c.runtime.ociRuntime.killContainer(c, signal)
} }
@ -321,7 +324,7 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir
// TODO handle this better // TODO handle this better
return errors.Wrapf(err, "error saving exec sessions %s for container %s", sessionID, c.ID()) return errors.Wrapf(err, "error saving exec sessions %s for container %s", sessionID, c.ID())
} }
c.newContainerEvent(events.Exec)
logrus.Debugf("Successfully started exec session %s in container %s", sessionID, c.ID()) logrus.Debugf("Successfully started exec session %s in container %s", sessionID, c.ID())
// Unlock so other processes can use the container // Unlock so other processes can use the container
@ -351,7 +354,6 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir
if err := c.save(); err != nil { if err := c.save(); err != nil {
logrus.Errorf("Error removing exec session %s from container %s state: %v", sessionID, c.ID(), err) logrus.Errorf("Error removing exec session %s from container %s state: %v", sessionID, c.ID(), err)
} }
return waitErr return waitErr
} }
@ -390,7 +392,7 @@ func (c *Container) Attach(streams *AttachStreams, keys string, resize <-chan re
c.state.State != ContainerStateExited { c.state.State != ContainerStateExited {
return errors.Wrapf(ErrCtrStateInvalid, "can only attach to created or running containers") return errors.Wrapf(ErrCtrStateInvalid, "can only attach to created or running containers")
} }
defer c.newContainerEvent(events.Attach)
return c.attach(streams, keys, resize, false) return c.attach(streams, keys, resize, false)
} }
@ -405,7 +407,7 @@ func (c *Container) Mount() (string, error) {
return "", err return "", err
} }
} }
defer c.newContainerEvent(events.Mount)
return c.mount() return c.mount()
} }
@ -435,6 +437,7 @@ func (c *Container) Unmount(force bool) error {
return errors.Wrapf(ErrInternal, "can't unmount %s last mount, it is still in use", c.ID()) return errors.Wrapf(ErrInternal, "can't unmount %s last mount, it is still in use", c.ID())
} }
} }
defer c.newContainerEvent(events.Unmount)
return c.unmount(force) return c.unmount(force)
} }
@ -455,7 +458,7 @@ func (c *Container) Pause() error {
if c.state.State != ContainerStateRunning { if c.state.State != ContainerStateRunning {
return errors.Wrapf(ErrCtrStateInvalid, "%q is not running, can't pause", c.state.State) return errors.Wrapf(ErrCtrStateInvalid, "%q is not running, can't pause", c.state.State)
} }
defer c.newContainerEvent(events.Pause)
return c.pause() return c.pause()
} }
@ -473,7 +476,7 @@ func (c *Container) Unpause() error {
if c.state.State != ContainerStatePaused { if c.state.State != ContainerStatePaused {
return errors.Wrapf(ErrCtrStateInvalid, "%q is not paused, can't unpause", c.ID()) return errors.Wrapf(ErrCtrStateInvalid, "%q is not paused, can't unpause", c.ID())
} }
defer c.newContainerEvent(events.Unpause)
return c.unpause() return c.unpause()
} }
@ -488,7 +491,7 @@ func (c *Container) Export(path string) error {
return err return err
} }
} }
defer c.newContainerEvent(events.Export)
return c.export(path) return c.export(path)
} }
@ -542,7 +545,6 @@ func (c *Container) Inspect(size bool) (*inspect.ContainerInspectData, error) {
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "error getting graph driver info %q", c.ID()) return nil, errors.Wrapf(err, "error getting graph driver info %q", c.ID())
} }
return c.getContainerInspectData(size, driverData) return c.getContainerInspectData(size, driverData)
} }
@ -574,6 +576,7 @@ func (c *Container) WaitWithInterval(waitTimeout time.Duration) (int32, error) {
return 0, err return 0, err
} }
exitCode := c.state.ExitCode exitCode := c.state.ExitCode
c.newContainerEvent(events.Wait)
return exitCode, nil return exitCode, nil
} }
@ -597,7 +600,7 @@ func (c *Container) Cleanup(ctx context.Context) error {
if len(c.state.ExecSessions) != 0 { if len(c.state.ExecSessions) != 0 {
return errors.Wrapf(ErrCtrStateInvalid, "container %s has active exec sessions, refusing to clean up", c.ID()) return errors.Wrapf(ErrCtrStateInvalid, "container %s has active exec sessions, refusing to clean up", c.ID())
} }
defer c.newContainerEvent(events.Cleanup)
return c.cleanup(ctx) return c.cleanup(ctx)
} }
@ -667,7 +670,7 @@ func (c *Container) Sync() error {
} }
} }
} }
defer c.newContainerEvent(events.Sync)
return nil return nil
} }
@ -772,7 +775,6 @@ func (c *Container) Refresh(ctx context.Context) error {
return err return err
} }
} }
return nil return nil
} }
@ -800,7 +802,7 @@ func (c *Container) Checkpoint(ctx context.Context, options ContainerCheckpointO
return err return err
} }
} }
defer c.newContainerEvent(events.Checkpoint)
return c.checkpoint(ctx, options) return c.checkpoint(ctx, options)
} }
@ -815,6 +817,6 @@ func (c *Container) Restore(ctx context.Context, options ContainerCheckpointOpti
return err return err
} }
} }
defer c.newContainerEvent(events.Restore)
return c.restore(ctx, options) return c.restore(ctx, options)
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/containers/buildah" "github.com/containers/buildah"
"github.com/containers/buildah/util" "github.com/containers/buildah/util"
is "github.com/containers/image/storage" is "github.com/containers/image/storage"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -177,5 +178,6 @@ func (c *Container) Commit(ctx context.Context, destImage string, options Contai
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer c.newContainerEvent(events.Commit)
return c.runtime.imageRuntime.NewFromLocal(id) return c.runtime.imageRuntime.NewFromLocal(id)
} }

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/ctime" "github.com/containers/libpod/pkg/ctime"
"github.com/containers/libpod/pkg/hooks" "github.com/containers/libpod/pkg/hooks"
"github.com/containers/libpod/pkg/hooks/exec" "github.com/containers/libpod/pkg/hooks/exec"
@ -824,7 +825,7 @@ func (c *Container) init(ctx context.Context) error {
if err := c.save(); err != nil { if err := c.save(); err != nil {
return err return err
} }
defer c.newContainerEvent(events.Init)
return c.completeNetworkSetup() return c.completeNetworkSetup()
} }
@ -1022,7 +1023,6 @@ func (c *Container) restartWithTimeout(ctx context.Context, timeout uint) (err e
return err return err
} }
} }
return c.start() return c.start()
} }

81
libpod/events.go Normal file
View File

@ -0,0 +1,81 @@
package libpod
import (
"github.com/containers/libpod/libpod/events"
"github.com/hpcloud/tail"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// newContainerEvent creates a new event based on a container
func (c *Container) newContainerEvent(status events.Status) {
e := events.NewEvent(status)
e.ID = c.ID()
e.Name = c.Name()
e.Image = c.config.RootfsImageName
e.Type = events.Container
if err := e.Write(c.runtime.config.EventsLogFilePath); err != nil {
logrus.Errorf("unable to write event to %s", c.runtime.config.EventsLogFilePath)
}
}
// newPodEvent creates a new event for a libpod pod
func (p *Pod) newPodEvent(status events.Status) {
e := events.NewEvent(status)
e.ID = p.ID()
e.Name = p.Name()
e.Type = events.Pod
if err := e.Write(p.runtime.config.EventsLogFilePath); err != nil {
logrus.Errorf("unable to write event to %s", p.runtime.config.EventsLogFilePath)
}
}
// newVolumeEvent creates a new event for a libpod volume
func (v *Volume) newVolumeEvent(status events.Status) {
e := events.NewEvent(status)
e.Name = v.Name()
e.Type = events.Volume
if err := e.Write(v.runtime.config.EventsLogFilePath); err != nil {
logrus.Errorf("unable to write event to %s", v.runtime.config.EventsLogFilePath)
}
}
// Events is a wrapper function for everyone to begin tailing the events log
// with options
func (r *Runtime) Events(fromStart, stream bool, options []events.EventFilter, eventChannel chan *events.Event) error {
t, err := r.getTail(fromStart, stream)
if err != nil {
return err
}
for line := range t.Lines {
event, err := events.NewEventFromString(line.Text)
if err != nil {
return err
}
switch event.Type {
case events.Image, events.Volume, events.Pod, events.Container:
// no-op
default:
return errors.Errorf("event type %s is not valid in %s", event.Type.String(), r.GetConfig().EventsLogFilePath)
}
include := true
for _, filter := range options {
include = include && filter(event)
}
if include {
eventChannel <- event
}
}
close(eventChannel)
return nil
}
func (r *Runtime) getTail(fromStart, stream bool) (*tail.Tail, error) {
reopen := true
seek := tail.SeekInfo{Offset: 0, Whence: 2}
if fromStart || !stream {
seek.Whence = 0
reopen = false
}
return tail.TailFile(r.config.EventsLogFilePath, tail.Config{ReOpen: reopen, Follow: stream, Location: &seek})
}

264
libpod/events/events.go Normal file
View File

@ -0,0 +1,264 @@
package events
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/pkg/errors"
)
// Event describes the attributes of a libpod event
type Event struct {
// ContainerExitCode is for storing the exit code of a container which can
// be used for "internal" event notification
ContainerExitCode int
// ID can be for the container, image, volume, etc
ID string
// Image used where applicable
Image string
// Name where applicable
Name string
// Status describes the event that occurred
Status Status
// Time the event occurred
Time time.Time
// Type of event that occurred
Type Type
}
// Type of event that occurred (container, volume, image, pod, etc)
type Type string
// Status describes the actual event action (stop, start, create, kill)
type Status string
const (
// If you add or subtract any values to the following lists, make sure you also update
// the switch statements below and the enums for EventType or EventStatus in the
// varlink description file.
// Container - event is related to containers
Container Type = "container"
// Image - event is related to images
Image Type = "image"
// Pod - event is related to pods
Pod Type = "pod"
// Volume - event is related to volumes
Volume Type = "volume"
// Attach ...
Attach Status = "attach"
// Checkpoint ...
Checkpoint Status = "checkpoint"
// Cleanup ...
Cleanup Status = "cleanup"
// Commit ...
Commit Status = "commit"
// Create ...
Create Status = "create"
// Exec ...
Exec Status = "exec"
// Export ...
Export Status = "export"
// History ...
History Status = "history"
// Import ...
Import Status = "import"
// Init ...
Init Status = "init"
// Kill ...
Kill Status = "kill"
// LoadFromArchive ...
LoadFromArchive Status = "status"
// Mount ...
Mount Status = "mount"
// Pause ...
Pause Status = "pause"
// Prune ...
Prune Status = "prune"
// Pull ...
Pull Status = "pull"
// Push ...
Push Status = "push"
// Remove ...
Remove Status = "remove"
// Restore ...
Restore Status = "restore"
// Save ...
Save Status = "save"
// Start ...
Start Status = "start"
// Stop ...
Stop Status = "stop"
// Sync ...
Sync Status = "sync"
// Tag ...
Tag Status = "tag"
// Unmount ...
Unmount Status = "unmount"
// Unpause ...
Unpause Status = "unpause"
// Untag ...
Untag Status = "untag"
// Wait ...
Wait Status = "wait"
)
// EventFilter for filtering events
type EventFilter func(*Event) bool
// NewEvent creates a event struct and populates with
// the given status and time.
func NewEvent(status Status) Event {
return Event{
Status: status,
Time: time.Now(),
}
}
// Write will record the event to the given path
func (e *Event) Write(path string) error {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700)
if err != nil {
return err
}
defer f.Close()
eventJSONString, err := e.ToJSONString()
if err != nil {
return err
}
if _, err := f.WriteString(fmt.Sprintf("%s\n", eventJSONString)); err != nil {
return err
}
return nil
}
// Recycle checks if the event log has reach a limit and if so
// renames the current log and starts a new one. The remove bool
// indicates the old log file should be deleted.
func (e *Event) Recycle(path string, remove bool) error {
return errors.New("not implemented")
}
// ToJSONString returns the event as a json'ified string
func (e *Event) ToJSONString() (string, error) {
b, err := json.Marshal(e)
return string(b), err
}
// ToHumanReadable returns human readable event as a formatted string
func (e *Event) ToHumanReadable() string {
var humanFormat string
switch e.Type {
case Container, Pod:
humanFormat = fmt.Sprintf("%s %s %s %s (image=%s, name=%s)", e.Time, e.Type, e.Status, e.ID, e.Image, e.Name)
case Image:
humanFormat = fmt.Sprintf("%s %s %s %s %s", e.Time, e.Type, e.Status, e.ID, e.Name)
case Volume:
humanFormat = fmt.Sprintf("%s %s %s %s", e.Time, e.Type, e.Status, e.Name)
}
return humanFormat
}
// NewEventFromString takes stringified json and converts
// it to an event
func NewEventFromString(event string) (*Event, error) {
e := Event{}
if err := json.Unmarshal([]byte(event), &e); err != nil {
return nil, err
}
return &e, nil
}
// ToString converts a Type to a string
func (t Type) String() string {
return string(t)
}
// ToString converts a status to a string
func (s Status) String() string {
return string(s)
}
// StringToType converts string to an EventType
func StringToType(name string) (Type, error) {
switch name {
case Container.String():
return Container, nil
case Image.String():
return Image, nil
case Pod.String():
return Pod, nil
case Volume.String():
return Volume, nil
}
return "", errors.Errorf("unknown event type %s", name)
}
// StringToStatus converts a string to an Event Status
// TODO if we add more events, we might consider a go-generator to
// create the switch statement
func StringToStatus(name string) (Status, error) {
switch name {
case Attach.String():
return Attach, nil
case Checkpoint.String():
return Checkpoint, nil
case Restore.String():
return Restore, nil
case Cleanup.String():
return Cleanup, nil
case Commit.String():
return Commit, nil
case Create.String():
return Create, nil
case Exec.String():
return Exec, nil
case Export.String():
return Export, nil
case History.String():
return History, nil
case Import.String():
return Import, nil
case Init.String():
return Init, nil
case Kill.String():
return Kill, nil
case LoadFromArchive.String():
return LoadFromArchive, nil
case Mount.String():
return Mount, nil
case Pause.String():
return Pause, nil
case Prune.String():
return Prune, nil
case Pull.String():
return Pull, nil
case Push.String():
return Push, nil
case Remove.String():
return Remove, nil
case Save.String():
return Save, nil
case Start.String():
return Start, nil
case Stop.String():
return Stop, nil
case Sync.String():
return Sync, nil
case Tag.String():
return Tag, nil
case Unmount.String():
return Unmount, nil
case Unpause.String():
return Unpause, nil
case Untag.String():
return Untag, nil
case Wait.String():
return Wait, nil
}
return "", errors.Errorf("unknown event status %s", name)
}

View File

@ -24,12 +24,13 @@ import (
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/libpod/libpod/common" "github.com/containers/libpod/libpod/common"
"github.com/containers/libpod/libpod/driver" "github.com/containers/libpod/libpod/driver"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/inspect" "github.com/containers/libpod/pkg/inspect"
"github.com/containers/libpod/pkg/registries" "github.com/containers/libpod/pkg/registries"
"github.com/containers/libpod/pkg/util" "github.com/containers/libpod/pkg/util"
"github.com/containers/storage" "github.com/containers/storage"
"github.com/containers/storage/pkg/reexec" "github.com/containers/storage/pkg/reexec"
digest "github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go"
@ -64,6 +65,7 @@ type Image struct {
type Runtime struct { type Runtime struct {
store storage.Store store storage.Store
SignaturePolicyPath string SignaturePolicyPath string
EventsLogFilePath string
} }
// ErrRepoTagNotFound is the error returned when the image id given doesn't match a rep tag in store // ErrRepoTagNotFound is the error returned when the image id given doesn't match a rep tag in store
@ -195,7 +197,7 @@ func (ir *Runtime) LoadFromArchiveReference(ctx context.Context, srcRef types.Im
newImage.image = img newImage.image = img
newImages = append(newImages, &newImage) newImages = append(newImages, &newImage)
} }
ir.newImageEvent(events.LoadFromArchive, "")
return newImages, nil return newImages, nil
} }
@ -371,6 +373,7 @@ func (i *Image) Remove(force bool) error {
} }
parent = nextParent parent = nextParent
} }
defer i.newImageEvent(events.Remove)
return nil return nil
} }
@ -494,6 +497,7 @@ func (i *Image) TagImage(tag string) error {
return err return err
} }
i.reloadImage() i.reloadImage()
defer i.newImageEvent(events.Tag)
return nil return nil
} }
@ -514,6 +518,7 @@ func (i *Image) UntagImage(tag string) error {
return err return err
} }
i.reloadImage() i.reloadImage()
defer i.newImageEvent(events.Untag)
return nil return nil
} }
@ -562,6 +567,7 @@ func (i *Image) PushImageToReference(ctx context.Context, dest types.ImageRefere
if err != nil { if err != nil {
return errors.Wrapf(err, "Error copying image to the remote destination") return errors.Wrapf(err, "Error copying image to the remote destination")
} }
defer i.newImageEvent(events.Push)
return nil return nil
} }
@ -711,7 +717,6 @@ func (i *Image) History(ctx context.Context) ([]*History, error) {
Comment: oci.History[i].Comment, Comment: oci.History[i].Comment,
}) })
} }
return allHistory, nil return allHistory, nil
} }
@ -927,7 +932,11 @@ func (ir *Runtime) Import(ctx context.Context, path, reference string, writer io
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ir.NewFromLocal(reference) newImage, err := ir.NewFromLocal(reference)
if err == nil {
defer newImage.newImageEvent(events.Import)
}
return newImage, err
} }
// MatchRepoTag takes a string and tries to match it against an // MatchRepoTag takes a string and tries to match it against an
@ -1148,7 +1157,7 @@ func (i *Image) Save(ctx context.Context, source, format, output string, moreTag
} }
return errors.Wrapf(err, "unable to save %q", source) return errors.Wrapf(err, "unable to save %q", source)
} }
defer i.newImageEvent(events.Save)
return nil return nil
} }
@ -1180,3 +1189,26 @@ func (i *Image) GetHealthCheck(ctx context.Context) (*manifest.Schema2HealthConf
} }
return configBlob.ContainerConfig.Healthcheck, nil return configBlob.ContainerConfig.Healthcheck, nil
} }
// newImageEvent creates a new event based on an image
func (ir *Runtime) newImageEvent(status events.Status, name string) {
e := events.NewEvent(status)
e.Type = events.Image
e.Name = name
if err := e.Write(ir.EventsLogFilePath); err != nil {
logrus.Infof("unable to write event to %s", ir.EventsLogFilePath)
}
}
// newImageEvent creates a new event based on an image
func (i *Image) newImageEvent(status events.Status) {
e := events.NewEvent(status)
e.ID = i.ID()
e.Type = events.Image
if len(i.Names()) > 0 {
e.Name = i.Names()[0]
}
if err := e.Write(i.imageruntime.EventsLogFilePath); err != nil {
logrus.Infof("unable to write event to %s", i.imageruntime.EventsLogFilePath)
}
}

View File

@ -1,6 +1,9 @@
package image package image
import "github.com/pkg/errors" import (
"github.com/containers/libpod/libpod/events"
"github.com/pkg/errors"
)
// GetPruneImages returns a slice of images that have no names/unused // GetPruneImages returns a slice of images that have no names/unused
func (ir *Runtime) GetPruneImages(all bool) ([]*Image, error) { func (ir *Runtime) GetPruneImages(all bool) ([]*Image, error) {
@ -41,6 +44,7 @@ func (ir *Runtime) PruneImages(all bool) ([]string, error) {
if err := p.Remove(true); err != nil { if err := p.Remove(true); err != nil {
return nil, errors.Wrap(err, "failed to prune image") return nil, errors.Wrap(err, "failed to prune image")
} }
defer p.newImageEvent(events.Prune)
prunedCids = append(prunedCids, p.ID()) prunedCids = append(prunedCids, p.ID())
} }
return prunedCids, nil return prunedCids, nil

View File

@ -17,6 +17,7 @@ import (
"github.com/containers/image/transports" "github.com/containers/image/transports"
"github.com/containers/image/transports/alltransports" "github.com/containers/image/transports/alltransports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/registries" "github.com/containers/libpod/pkg/registries"
multierror "github.com/hashicorp/go-multierror" multierror "github.com/hashicorp/go-multierror"
opentracing "github.com/opentracing/opentracing-go" opentracing "github.com/opentracing/opentracing-go"
@ -273,6 +274,7 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
} }
} else { } else {
if !goal.pullAllPairs { if !goal.pullAllPairs {
ir.newImageEvent(events.Pull, "")
return []string{imageInfo.image}, nil return []string{imageInfo.image}, nil
} }
images = append(images, imageInfo.image) images = append(images, imageInfo.image)
@ -293,6 +295,9 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
} }
return nil, pullErrors return nil, pullErrors
} }
if len(images) > 0 {
defer ir.newImageEvent(events.Pull, images[0])
}
return images, nil return images, nil
} }

View File

@ -286,7 +286,6 @@ func WithTmpDir(dir string) RuntimeOption {
if rt.valid { if rt.valid {
return ErrRuntimeFinalized return ErrRuntimeFinalized
} }
rt.config.TmpDir = dir rt.config.TmpDir = dir
rt.configuredFrom.libpodTmpDirSet = true rt.configuredFrom.libpodTmpDirSet = true

View File

@ -3,6 +3,7 @@ package libpod
import ( import (
"context" "context"
"github.com/containers/libpod/libpod/events"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/ulule/deepcopier" "github.com/ulule/deepcopier"
@ -58,7 +59,7 @@ func (p *Pod) Start(ctx context.Context) (map[string]error, error) {
if len(ctrErrors) > 0 { if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error starting some containers") return ctrErrors, errors.Wrapf(ErrCtrExists, "error starting some containers")
} }
defer p.newPodEvent(events.Start)
return nil, nil return nil, nil
} }
@ -138,7 +139,7 @@ func (p *Pod) StopWithTimeout(ctx context.Context, cleanup bool, timeout int) (m
if len(ctrErrors) > 0 { if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error stopping some containers") return ctrErrors, errors.Wrapf(ErrCtrExists, "error stopping some containers")
} }
defer p.newPodEvent(events.Stop)
return nil, nil return nil, nil
} }
@ -197,7 +198,7 @@ func (p *Pod) Pause() (map[string]error, error) {
if len(ctrErrors) > 0 { if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error pausing some containers") return ctrErrors, errors.Wrapf(ErrCtrExists, "error pausing some containers")
} }
defer p.newPodEvent(events.Pause)
return nil, nil return nil, nil
} }
@ -257,6 +258,7 @@ func (p *Pod) Unpause() (map[string]error, error) {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error unpausing some containers") return ctrErrors, errors.Wrapf(ErrCtrExists, "error unpausing some containers")
} }
defer p.newPodEvent(events.Unpause)
return nil, nil return nil, nil
} }
@ -309,7 +311,8 @@ func (p *Pod) Restart(ctx context.Context) (map[string]error, error) {
if len(ctrErrors) > 0 { if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error stopping some containers") return ctrErrors, errors.Wrapf(ErrCtrExists, "error stopping some containers")
} }
p.newPodEvent(events.Stop)
p.newPodEvent(events.Start)
return nil, nil return nil, nil
} }
@ -367,7 +370,7 @@ func (p *Pod) Kill(signal uint) (map[string]error, error) {
if len(ctrErrors) > 0 { if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error killing some containers") return ctrErrors, errors.Wrapf(ErrCtrExists, "error killing some containers")
} }
defer p.newPodEvent(events.Kill)
return nil, nil return nil, nil
} }

View File

@ -223,6 +223,9 @@ type RuntimeConfig struct {
// NumLocks is the number of locks to make available for containers and // NumLocks is the number of locks to make available for containers and
// pods. // pods.
NumLocks uint32 `toml:"num_locks,omitempty"` NumLocks uint32 `toml:"num_locks,omitempty"`
// EventsLogFilePath is where the events log is stored.
EventsLogFilePath string `toml:-"events_logfile_path"`
} }
// runtimeConfiguredFrom is a struct used during early runtime init to help // runtimeConfiguredFrom is a struct used during early runtime init to help
@ -459,7 +462,6 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
} }
} }
} }
return runtime, nil return runtime, nil
} }
@ -535,6 +537,7 @@ func NewRuntimeFromConfig(configPath string, options ...RuntimeOption) (runtime
// Make a new runtime based on the given configuration // Make a new runtime based on the given configuration
// Sets up containers/storage, state store, OCI runtime // Sets up containers/storage, state store, OCI runtime
func makeRuntime(runtime *Runtime) (err error) { func makeRuntime(runtime *Runtime) (err error) {
runtime.config.EventsLogFilePath = filepath.Join(runtime.config.TmpDir, "events", "events.log")
// Backward compatibility for `runtime_path` // Backward compatibility for `runtime_path`
if runtime.config.RuntimePath != nil { if runtime.config.RuntimePath != nil {
@ -736,6 +739,9 @@ func makeRuntime(runtime *Runtime) (err error) {
// Setting signaturepolicypath // Setting signaturepolicypath
ir.SignaturePolicyPath = runtime.config.SignaturePolicyPath ir.SignaturePolicyPath = runtime.config.SignaturePolicyPath
// Set logfile path for events
ir.EventsLogFilePath = runtime.config.EventsLogFilePath
defer func() { defer func() {
if err != nil && store != nil { if err != nil && store != nil {
// Don't forcibly shut down // Don't forcibly shut down
@ -768,6 +774,14 @@ func makeRuntime(runtime *Runtime) (err error) {
} }
} }
// Create events log dir
if err := os.MkdirAll(filepath.Dir(runtime.config.EventsLogFilePath), 0700); err != nil {
// The directory is allowed to exist
if !os.IsExist(err) {
return errors.Wrapf(err, "error creating events dirs %s", filepath.Dir(runtime.config.EventsLogFilePath))
}
}
// Make an OCI runtime to perform container operations // Make an OCI runtime to perform container operations
ociRuntime, err := newOCIRuntime(runtime.ociRuntimePath, ociRuntime, err := newOCIRuntime(runtime.ociRuntimePath,
runtime.conmonPath, runtime.config.ConmonEnvVars, runtime.conmonPath, runtime.config.ConmonEnvVars,

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/rootless"
"github.com/containers/storage" "github.com/containers/storage"
@ -228,6 +229,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
return nil, err return nil, err
} }
} }
ctr.newContainerEvent(events.Create)
return ctr, nil return ctr, nil
} }
@ -239,7 +241,6 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
func (r *Runtime) RemoveContainer(ctx context.Context, c *Container, force bool, removeVolume bool) error { func (r *Runtime) RemoveContainer(ctx context.Context, c *Container, force bool, removeVolume bool) error {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
return r.removeContainer(ctx, c, force, removeVolume) return r.removeContainer(ctx, c, force, removeVolume)
} }
@ -430,6 +431,7 @@ func (r *Runtime) removeContainer(ctx context.Context, c *Container, force bool,
} }
} }
c.newContainerEvent(events.Remove)
return cleanupErr return cleanupErr
} }

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/containerd/cgroups" "github.com/containerd/cgroups"
"github.com/containers/libpod/libpod/events"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -121,7 +122,7 @@ func (r *Runtime) NewPod(ctx context.Context, options ...PodCreateOption) (*Pod,
return nil, err return nil, err
} }
} }
pod.newPodEvent(events.Create)
return pod, nil return pod, nil
} }
@ -307,6 +308,6 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool)
// Mark pod invalid // Mark pod invalid
p.valid = false p.valid = false
p.newPodEvent(events.Remove)
return nil return nil
} }

View File

@ -2,9 +2,11 @@ package libpod
import ( import (
"context" "context"
"strings"
"github.com/containers/libpod/libpod/events"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"strings"
) )
// Contains the public Runtime API for volumes // Contains the public Runtime API for volumes
@ -34,7 +36,6 @@ func (r *Runtime) RemoveVolume(ctx context.Context, v *Volume, force bool) error
return nil return nil
} }
} }
return r.removeVolume(ctx, v, force) return r.removeVolume(ctx, v, force)
} }
@ -171,6 +172,7 @@ func (r *Runtime) PruneVolumes(ctx context.Context) ([]string, []error) {
} }
continue continue
} }
vol.newVolumeEvent(events.Prune)
prunedIDs = append(prunedIDs, vol.Name()) prunedIDs = append(prunedIDs, vol.Name())
} }
return prunedIDs, pruneErrors return prunedIDs, pruneErrors

View File

@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/containers/libpod/libpod/events"
"github.com/containers/storage/pkg/stringid" "github.com/containers/storage/pkg/stringid"
"github.com/opencontainers/selinux/go-selinux/label" "github.com/opencontainers/selinux/go-selinux/label"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -73,7 +74,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
if err := r.state.AddVolume(volume); err != nil { if err := r.state.AddVolume(volume); err != nil {
return nil, errors.Wrapf(err, "error adding volume to state") return nil, errors.Wrapf(err, "error adding volume to state")
} }
defer volume.newVolumeEvent(events.Create)
return volume, nil return volume, nil
} }
@ -118,7 +119,7 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool) error
return errors.Wrapf(err, "error cleaning up volume storage for %q", v.Name()) return errors.Wrapf(err, "error cleaning up volume storage for %q", v.Name())
} }
defer v.newVolumeEvent(events.Remove)
logrus.Debugf("Removed volume %s", v.Name()) logrus.Debugf("Removed volume %s", v.Name())
return nil return nil
} }

View File

@ -3,11 +3,13 @@
package adapter package adapter
import ( import (
"bufio"
"context" "context"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"strconv" "strconv"
"text/template"
"github.com/containers/buildah" "github.com/containers/buildah"
"github.com/containers/buildah/imagebuildah" "github.com/containers/buildah/imagebuildah"
@ -16,7 +18,9 @@ import (
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/rootless"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -377,3 +381,52 @@ func (r *LocalRuntime) JoinOrCreateRootlessPod(pod *Pod) (bool, int, error) {
return rootless.BecomeRootInUserNSWithOpts(&opts) return rootless.BecomeRootInUserNSWithOpts(&opts)
} }
// Events is a wrapper to libpod to obtain libpod/podman events
func (r *LocalRuntime) Events(c *cliconfig.EventValues) error {
var (
fromStart bool
eventsError error
)
options, err := shared.GenerateEventOptions(c.Filter, c.Since, c.Until)
if err != nil {
return errors.Wrapf(err, "unable to generate event options")
}
tmpl, err := template.New("events").Parse(c.Format)
if err != nil {
return err
}
if len(c.Since) > 0 || len(c.Until) > 0 {
fromStart = true
}
eventChannel := make(chan *events.Event)
go func() {
eventsError = r.Runtime.Events(fromStart, c.Stream, options, eventChannel)
}()
if eventsError != nil {
return eventsError
}
if err != nil {
return errors.Wrapf(err, "unable to tail the events log")
}
w := bufio.NewWriter(os.Stdout)
for event := range eventChannel {
if len(c.Format) > 0 {
if err := tmpl.Execute(w, event); err != nil {
return err
}
} else {
if _, err := w.Write([]byte(event.ToHumanReadable())); err != nil {
return err
}
}
if _, err := w.Write([]byte("\n")); err != nil {
return err
}
if err := w.Flush(); err != nil {
return err
}
}
return nil
}

View File

@ -10,6 +10,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"text/template"
"time" "time"
"github.com/containers/buildah/imagebuildah" "github.com/containers/buildah/imagebuildah"
@ -18,6 +19,7 @@ import (
"github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/cmd/podman/varlink"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/utils" "github.com/containers/libpod/utils"
"github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/archive"
@ -758,3 +760,69 @@ func (r *LocalRuntime) JoinOrCreateRootlessPod(pod *Pod) (bool, int, error) {
// Nothing to do in the remote case // Nothing to do in the remote case
return true, 0, nil return true, 0, nil
} }
// Events monitors libpod/podman events over a varlink connection
func (r *LocalRuntime) Events(c *cliconfig.EventValues) error {
reply, err := iopodman.GetEvents().Send(r.Conn, uint64(varlink.More), c.Filter, c.Since, c.Stream, c.Until)
if err != nil {
return errors.Wrapf(err, "unable to obtain events")
}
w := bufio.NewWriter(os.Stdout)
tmpl, err := template.New("events").Parse(c.Format)
if err != nil {
return err
}
for {
returnedEvent, flags, err := reply()
if err != nil {
// When the error handling is back into podman, we can flip this to a better way to check
// for problems. For now, this works.
return err
}
if returnedEvent.Time == "" && returnedEvent.Status == "" && returnedEvent.Type == "" {
// We got a blank event return, signals end of stream in certain cases
break
}
eTime, err := time.Parse(time.RFC3339Nano, returnedEvent.Time)
if err != nil {
return errors.Wrapf(err, "unable to parse time of event %s", returnedEvent.Time)
}
eType, err := events.StringToType(returnedEvent.Type)
if err != nil {
return err
}
eStatus, err := events.StringToStatus(returnedEvent.Status)
if err != nil {
return err
}
event := events.Event{
ID: returnedEvent.Id,
Image: returnedEvent.Image,
Name: returnedEvent.Name,
Status: eStatus,
Time: eTime,
Type: eType,
}
if len(c.Format) > 0 {
if err := tmpl.Execute(w, event); err != nil {
return err
}
} else {
if _, err := w.Write([]byte(event.ToHumanReadable())); err != nil {
return err
}
}
if _, err := w.Write([]byte("\n")); err != nil {
return err
}
if err := w.Flush(); err != nil {
return err
}
if flags&varlink.Continues == 0 {
break
}
}
return nil
}

View File

@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
"time"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/containers/image/types" "github.com/containers/image/types"
@ -347,3 +348,25 @@ func StorageConfigFile() string {
} }
return storage.DefaultConfigFile return storage.DefaultConfigFile
} }
// ParseInputTime takes the users input and to determine if it is valid and
// returns a time format and error. The input is compared to known time formats
// or a duration which implies no-duration
func ParseInputTime(inputTime string) (time.Time, error) {
timeFormats := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05", "2006-01-02T15:04:05.999999999",
"2006-01-02Z07:00", "2006-01-02"}
// iterate the supported time formats
for _, tf := range timeFormats {
t, err := time.Parse(tf, inputTime)
if err == nil {
return t, nil
}
}
// input might be a duration
duration, err := time.ParseDuration(inputTime)
if err != nil {
return time.Time{}, errors.Errorf("unable to interpret time value")
}
return time.Now().Add(-duration), nil
}

56
pkg/varlinkapi/events.go Normal file
View File

@ -0,0 +1,56 @@
package varlinkapi
import (
"fmt"
"time"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/cmd/podman/varlink"
"github.com/containers/libpod/libpod/events"
)
// GetEvents is a remote endpoint to get events from the event log
func (i *LibpodAPI) GetEvents(call iopodman.VarlinkCall, filter []string, since string, stream bool, until string) error {
var (
fromStart bool
eventsError error
event *events.Event
)
if call.WantsMore() {
call.Continues = true
}
filters, err := shared.GenerateEventOptions(filter, since, until)
if err != nil {
return call.ReplyErrorOccurred(err.Error())
}
if len(since) > 0 || len(until) > 0 {
fromStart = true
}
eventChannel := make(chan *events.Event)
go func() {
eventsError = i.Runtime.Events(fromStart, stream, filters, eventChannel)
}()
if eventsError != nil {
return call.ReplyErrorOccurred(err.Error())
}
for {
event = <-eventChannel
if event == nil {
call.Continues = false
break
}
call.ReplyGetEvents(iopodman.Event{
Id: event.ID,
Image: event.Image,
Name: event.Name,
Status: fmt.Sprintf("%s", event.Status),
Time: event.Time.Format(time.RFC3339Nano),
Type: fmt.Sprintf("%s", event.Type),
})
if !call.Continues {
// For a one-shot on events, we break out here
break
}
}
return call.ReplyGetEvents(iopodman.Event{})
}

View File

@ -45,6 +45,7 @@ type PodmanTestIntegration struct {
CgroupManager string CgroupManager string
Host HostOS Host HostOS
Timings []string Timings []string
TmpDir string
} }
var LockTmpDir string var LockTmpDir string
@ -245,6 +246,7 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration {
}, },
ConmonBinary: conmonBinary, ConmonBinary: conmonBinary,
CrioRoot: filepath.Join(tempDir, "crio"), CrioRoot: filepath.Join(tempDir, "crio"),
TmpDir: tempDir,
CNIConfigDir: CNIConfigDir, CNIConfigDir: CNIConfigDir,
OCIRuntime: ociRuntime, OCIRuntime: ociRuntime,
RunRoot: filepath.Join(tempDir, "crio-run"), RunRoot: filepath.Join(tempDir, "crio-run"),

116
test/e2e/events_test.go Normal file
View File

@ -0,0 +1,116 @@
package integration
import (
"fmt"
"os"
"strings"
. "github.com/containers/libpod/test/utils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Podman events", func() {
var (
tempdir string
err error
podmanTest *PodmanTestIntegration
)
BeforeEach(func() {
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)
}
podmanTest = PodmanTestCreate(tempdir)
podmanTest.RestoreAllArtifacts()
})
AfterEach(func() {
podmanTest.Cleanup()
f := CurrentGinkgoTestDescription()
timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
GinkgoWriter.Write([]byte(timedResult))
})
// For most, all, of these tests we do not "live" test following a log because it may make a fragile test
// system more complex. Instead we run the "events" and then verify that the events are processed correctly.
// Perhaps a future version of this test would put events in a go func and send output back over a channel
// while events occur.
It("podman events", func() {
_, ec, _ := podmanTest.RunLsContainer("")
Expect(ec).To(Equal(0))
result := podmanTest.Podman([]string{"events", "--stream=false"})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).To(BeZero())
})
It("podman events with an event filter", func() {
SkipIfRemote()
_, ec, _ := podmanTest.RunLsContainer("")
Expect(ec).To(Equal(0))
result := podmanTest.Podman([]string{"events", "--stream=false", "--filter", "event=start"})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).To(Equal(0))
Expect(len(result.OutputToStringArray())).To(Equal(1))
})
It("podman events with an event filter and container=cid", func() {
SkipIfRemote()
_, ec, cid := podmanTest.RunLsContainer("")
Expect(ec).To(Equal(0))
_, ec2, cid2 := podmanTest.RunLsContainer("")
Expect(ec2).To(Equal(0))
result := podmanTest.Podman([]string{"events", "--stream=false", "--filter", "event=start", "--filter", fmt.Sprintf("container=%s", cid)})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).To(Equal(0))
Expect(len(result.OutputToStringArray())).To(Equal(1))
Expect(!strings.Contains(result.OutputToString(), cid2))
})
It("podman events with a type", func() {
SkipIfRemote()
_, ec, _ := podmanTest.RunLsContainer("")
Expect(ec).To(Equal(0))
result := podmanTest.Podman([]string{"events", "--stream=false", "--filter", "type=pod"})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).To(Equal(0))
Expect(len(result.OutputToStringArray())).To(Equal(0))
})
It("podman events with a type", func() {
SkipIfRemote()
setup := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:foobar", ALPINE, "top"})
setup.WaitWithDefaultTimeout()
stop := podmanTest.Podman([]string{"pod", "stop", "foobar"})
stop.WaitWithDefaultTimeout()
Expect(stop.ExitCode()).To(Equal(0))
Expect(setup.ExitCode()).To(Equal(0))
result := podmanTest.Podman([]string{"events", "--stream=false", "--filter", "type=pod"})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).To(Equal(0))
fmt.Println(result.OutputToStringArray())
Expect(len(result.OutputToStringArray())).To(Equal(2))
})
It("podman events --since", func() {
_, ec, _ := podmanTest.RunLsContainer("")
Expect(ec).To(Equal(0))
result := podmanTest.Podman([]string{"events", "--stream=false", "--since", "1m"})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).To(BeZero())
})
It("podman events --until", func() {
_, ec, _ := podmanTest.RunLsContainer("")
Expect(ec).To(Equal(0))
test := podmanTest.Podman([]string{"events", "--help"})
test.WaitWithDefaultTimeout()
fmt.Println(test.OutputToStringArray())
result := podmanTest.Podman([]string{"events", "--stream=false", "--since", "1h"})
result.WaitWithDefaultTimeout()
Expect(result.ExitCode()).To(BeZero())
})
})

View File

@ -206,8 +206,8 @@ func PodmanTestCreate(tempDir string) *PodmanTestIntegration {
//MakeOptions assembles all the podman main options //MakeOptions assembles all the podman main options
func (p *PodmanTestIntegration) makeOptions(args []string) []string { func (p *PodmanTestIntegration) makeOptions(args []string) []string {
podmanOptions := strings.Split(fmt.Sprintf("--root %s --runroot %s --runtime %s --conmon %s --cni-config-dir %s --cgroup-manager %s", podmanOptions := strings.Split(fmt.Sprintf("--root %s --runroot %s --runtime %s --conmon %s --cni-config-dir %s --cgroup-manager %s --tmpdir %s",
p.CrioRoot, p.RunRoot, p.OCIRuntime, p.ConmonBinary, p.CNIConfigDir, p.CgroupManager), " ") p.CrioRoot, p.RunRoot, p.OCIRuntime, p.ConmonBinary, p.CNIConfigDir, p.CgroupManager, p.TmpDir), " ")
if os.Getenv("HOOK_OPTION") != "" { if os.Getenv("HOOK_OPTION") != "" {
podmanOptions = append(podmanOptions, os.Getenv("HOOK_OPTION")) podmanOptions = append(podmanOptions, os.Getenv("HOOK_OPTION"))
} }

View File

@ -44,6 +44,7 @@ There are other equivalents for these tools
| `docker container`|[`podman container`](./docs/podman-container.1.md) | | `docker container`|[`podman container`](./docs/podman-container.1.md) |
| `docker create` | [`podman create`](./docs/podman-create.1.md) | | `docker create` | [`podman create`](./docs/podman-create.1.md) |
| `docker diff` | [`podman diff`](./docs/podman-diff.1.md) | | `docker diff` | [`podman diff`](./docs/podman-diff.1.md) |
| `docker events` | [`podman events`](./docs/podman-events.1.md) |
| `docker export` | [`podman export`](./docs/podman-export.1.md) | | `docker export` | [`podman export`](./docs/podman-export.1.md) |
| `docker history` | [`podman history`](./docs/podman-history.1.md) | | `docker history` | [`podman history`](./docs/podman-history.1.md) |
| `docker image` | [`podman image`](./docs/podman-image.1.md) | | `docker image` | [`podman image`](./docs/podman-image.1.md) |
@ -89,7 +90,6 @@ Those Docker commands currently do not have equivalents in `podman`:
| Missing command | Description| | Missing command | Description|
| :--- | :--- | | :--- | :--- |
| `docker events` ||
| `docker network` || | `docker network` ||
| `docker node` || | `docker node` ||
| `docker plugin` | podman does not support plugins. We recommend you use alternative OCI Runtimes or OCI Runtime Hooks to alter behavior of podman.| | `docker plugin` | podman does not support plugins. We recommend you use alternative OCI Runtimes or OCI Runtime Hooks to alter behavior of podman.|