summaryrefslogtreecommitdiff
path: root/libpod
diff options
context:
space:
mode:
Diffstat (limited to 'libpod')
-rw-r--r--libpod/container_top_linux.go14
-rw-r--r--libpod/events.go69
-rw-r--r--libpod/events/config.go149
-rw-r--r--libpod/events/events.go148
-rw-r--r--libpod/events/events_linux.go20
-rw-r--r--libpod/events/events_unsupported.go10
-rw-r--r--libpod/events/filters.go114
-rw-r--r--libpod/events/journal_linux.go131
-rw-r--r--libpod/events/logfile.go65
-rw-r--r--libpod/events/nullout.go23
-rw-r--r--libpod/image/image.go249
-rw-r--r--libpod/image/image_test.go6
-rw-r--r--libpod/image/prune.go6
-rw-r--r--libpod/runtime.go24
-rw-r--r--libpod/runtime_img.go6
15 files changed, 809 insertions, 225 deletions
diff --git a/libpod/container_top_linux.go b/libpod/container_top_linux.go
index 9b0f156b5..b370495fe 100644
--- a/libpod/container_top_linux.go
+++ b/libpod/container_top_linux.go
@@ -7,8 +7,22 @@ import (
"strings"
"github.com/containers/psgo"
+ "github.com/pkg/errors"
)
+// Top gathers statistics about the running processes in a container. It returns a
+// []string for output
+func (c *Container) Top(descriptors []string) ([]string, error) {
+ conStat, err := c.State()
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to look up state for %s", c.ID())
+ }
+ if conStat != ContainerStateRunning {
+ return nil, errors.Errorf("top can only be used on running containers")
+ }
+ return c.GetContainerPidInformation(descriptors)
+}
+
// GetContainerPidInformation returns process-related data of all processes in
// the container. The output data can be controlled via the `descriptors`
// argument which expects format descriptors and supports all AIXformat
diff --git a/libpod/events.go b/libpod/events.go
index b6a277789..1b5c3bd99 100644
--- a/libpod/events.go
+++ b/libpod/events.go
@@ -1,14 +1,19 @@
package libpod
import (
- "os"
-
"github.com/containers/libpod/libpod/events"
- "github.com/hpcloud/tail"
- "github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
+// newEventer returns an eventer that can be used to read/write events
+func (r *Runtime) newEventer() (events.Eventer, error) {
+ options := events.EventerOptions{
+ EventerType: r.config.EventsLogger,
+ LogFilePath: r.config.EventsLogFilePath,
+ }
+ return events.NewEventer(options)
+}
+
// newContainerEvent creates a new event based on a container
func (c *Container) newContainerEvent(status events.Status) {
e := events.NewEvent(status)
@@ -16,8 +21,8 @@ func (c *Container) newContainerEvent(status events.Status) {
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)
+ if err := c.runtime.eventer.Write(e); err != nil {
+ logrus.Errorf("unable to write pod event: %q", err)
}
}
@@ -29,8 +34,8 @@ func (c *Container) newContainerExitedEvent(exitCode int32) {
e.Image = c.config.RootfsImageName
e.Type = events.Container
e.ContainerExitCode = int(exitCode)
- if err := e.Write(c.runtime.config.EventsLogFilePath); err != nil {
- logrus.Errorf("unable to write event to %s", c.runtime.config.EventsLogFilePath)
+ if err := c.runtime.eventer.Write(e); err != nil {
+ logrus.Errorf("unable to write pod event: %q", err)
}
}
@@ -40,8 +45,8 @@ func (p *Pod) newPodEvent(status events.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)
+ if err := p.runtime.eventer.Write(e); err != nil {
+ logrus.Errorf("unable to write pod event: %q", err)
}
}
@@ -50,51 +55,17 @@ 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)
+ if err := v.runtime.eventer.Write(e); err != nil {
+ logrus.Errorf("unable to write volume event: %q", err)
}
}
// 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 {
- if !r.valid {
- return ErrRuntimeStopped
- }
-
- t, err := r.getTail(fromStart, stream)
+func (r *Runtime) Events(options events.ReadOptions) error {
+ eventer, err := r.newEventer()
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.config.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: os.SEEK_END}
- if fromStart || !stream {
- seek.Whence = 0
- reopen = false
- }
- return tail.TailFile(r.config.EventsLogFilePath, tail.Config{ReOpen: reopen, Follow: stream, Location: &seek, Logger: tail.DiscardingLogger})
+ return eventer.Read(options)
}
diff --git a/libpod/events/config.go b/libpod/events/config.go
new file mode 100644
index 000000000..d3b6d8c50
--- /dev/null
+++ b/libpod/events/config.go
@@ -0,0 +1,149 @@
+package events
+
+import (
+ "time"
+)
+
+// EventerType ...
+type EventerType int
+
+const (
+ // LogFile indicates the event logger will be a logfile
+ LogFile EventerType = iota
+ // Journald indicates journald should be used to log events
+ Journald EventerType = iota
+)
+
+// 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
+}
+
+// EventerOptions describe options that need to be passed to create
+// an eventer
+type EventerOptions struct {
+ // EventerType describes whether to use journald or a file
+ EventerType string
+ // LogFilePath is the path to where the log file should reside if using
+ // the file logger
+ LogFilePath string
+}
+
+// Eventer is the interface for journald or file event logging
+type Eventer interface {
+ // Write an event to a backend
+ Write(event Event) error
+ // Read an event from the backend
+ Read(options ReadOptions) error
+}
+
+// ReadOptions describe the attributes needed to read event logs
+type ReadOptions struct {
+ // EventChannel is the comm path back to user
+ EventChannel chan *Event
+ // Filters are key/value pairs that describe to limit output
+ Filters []string
+ // FromStart means you start reading from the start of the logs
+ FromStart bool
+ // Since reads "since" the given time
+ Since string
+ // Stream is follow
+ Stream bool
+ // Until reads "until" the given time
+ Until string
+}
+
+// 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"
+ // Exited indicates that a container's process died
+ Exited Status = "died"
+ // Export ...
+ Export Status = "export"
+ // History ...
+ History Status = "history"
+ // Import ...
+ Import Status = "import"
+ // Init ...
+ Init Status = "init"
+ // Kill ...
+ Kill Status = "kill"
+ // LoadFromArchive ...
+ LoadFromArchive Status = "loadfromarchive"
+ // 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"
+)
+
+// EventFilter for filtering events
+type EventFilter func(*Event) bool
diff --git a/libpod/events/events.go b/libpod/events/events.go
index 074a3ba5b..e8c61faa0 100644
--- a/libpod/events/events.go
+++ b/libpod/events/events.go
@@ -6,109 +6,18 @@ import (
"os"
"time"
- "github.com/containers/storage"
+ "github.com/hpcloud/tail"
"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"
+// String returns a string representation of EventerType
+func (et EventerType) String() string {
+ if et == LogFile {
+ return "file"
- // Attach ...
- Attach Status = "attach"
- // Checkpoint ...
- Checkpoint Status = "checkpoint"
- // Cleanup ...
- Cleanup Status = "cleanup"
- // Commit ...
- Commit Status = "commit"
- // Create ...
- Create Status = "create"
- // Exec ...
- Exec Status = "exec"
- // Exited indicates that a container's process died
- Exited Status = "died"
- // 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"
-)
-
-// EventFilter for filtering events
-type EventFilter func(*Event) bool
+ }
+ return "journald"
+}
// NewEvent creates a event struct and populates with
// the given status and time.
@@ -119,30 +28,6 @@ func NewEvent(status Status) Event {
}
}
-// Write will record the event to the given path
-func (e *Event) Write(path string) error {
- // We need to lock events file
- lock, err := storage.GetLockfile(path + ".lock")
- if err != nil {
- return err
- }
- lock.Lock()
- defer lock.Unlock()
- 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.
@@ -172,7 +57,7 @@ func (e *Event) ToHumanReadable() string {
// NewEventFromString takes stringified json and converts
// it to an event
-func NewEventFromString(event string) (*Event, error) {
+func newEventFromJSONString(event string) (*Event, error) {
e := Event{}
if err := json.Unmarshal([]byte(event), &e); err != nil {
return nil, err
@@ -222,6 +107,7 @@ func StringToStatus(name string) (Status, error) {
case Commit.String():
return Commit, nil
case Create.String():
+
return Create, nil
case Exec.String():
return Exec, nil
@@ -270,3 +156,17 @@ func StringToStatus(name string) (Status, error) {
}
return "", errors.Errorf("unknown event status %s", name)
}
+
+func (e EventLogFile) getTail(options ReadOptions) (*tail.Tail, error) {
+ reopen := true
+ seek := tail.SeekInfo{Offset: 0, Whence: os.SEEK_END}
+ if options.FromStart || !options.Stream {
+ seek.Whence = 0
+ reopen = false
+ }
+ stream := options.Stream
+ if len(options.Until) > 0 {
+ stream = false
+ }
+ return tail.TailFile(e.options.LogFilePath, tail.Config{ReOpen: reopen, Follow: stream, Location: &seek, Logger: tail.DiscardingLogger})
+}
diff --git a/libpod/events/events_linux.go b/libpod/events/events_linux.go
new file mode 100644
index 000000000..d6898145c
--- /dev/null
+++ b/libpod/events/events_linux.go
@@ -0,0 +1,20 @@
+package events
+
+import (
+ "github.com/pkg/errors"
+ "strings"
+)
+
+// NewEventer creates an eventer based on the eventer type
+func NewEventer(options EventerOptions) (Eventer, error) {
+ var eventer Eventer
+ switch strings.ToUpper(options.EventerType) {
+ case strings.ToUpper(Journald.String()):
+ eventer = EventJournalD{options}
+ case strings.ToUpper(LogFile.String()):
+ eventer = EventLogFile{options}
+ default:
+ return eventer, errors.Errorf("unknown event logger type: %s", strings.ToUpper(options.EventerType))
+ }
+ return eventer, nil
+}
diff --git a/libpod/events/events_unsupported.go b/libpod/events/events_unsupported.go
new file mode 100644
index 000000000..5b32a1b4b
--- /dev/null
+++ b/libpod/events/events_unsupported.go
@@ -0,0 +1,10 @@
+// +build !linux
+
+package events
+
+import "github.com/pkg/errors"
+
+// NewEventer creates an eventer based on the eventer type
+func NewEventer(options EventerOptions) (Eventer, error) {
+ return nil, errors.New("this function is not available for your platform")
+}
diff --git a/libpod/events/filters.go b/libpod/events/filters.go
new file mode 100644
index 000000000..9a64082d1
--- /dev/null
+++ b/libpod/events/filters.go
@@ -0,0 +1,114 @@
+package events
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/containers/libpod/pkg/util"
+ "github.com/pkg/errors"
+)
+
+func generateEventFilter(filter, filterValue string) (func(e *Event) bool, error) {
+ switch strings.ToUpper(filter) {
+ case "CONTAINER":
+ return func(e *Event) bool {
+ if e.Type != Container {
+ return false
+ }
+ if e.Name == filterValue {
+ return true
+ }
+ return strings.HasPrefix(e.ID, filterValue)
+ }, nil
+ case "EVENT", "STATUS":
+ return func(e *Event) bool {
+ return fmt.Sprintf("%s", e.Status) == filterValue
+ }, nil
+ case "IMAGE":
+ return func(e *Event) bool {
+ if e.Type != Image {
+ return false
+ }
+ if e.Name == filterValue {
+ return true
+ }
+ return strings.HasPrefix(e.ID, filterValue)
+ }, nil
+ case "POD":
+ return func(e *Event) bool {
+ if e.Type != Pod {
+ return false
+ }
+ if e.Name == filterValue {
+ return true
+ }
+ return strings.HasPrefix(e.ID, filterValue)
+ }, nil
+ case "VOLUME":
+ return func(e *Event) bool {
+ if e.Type != Volume {
+ return false
+ }
+ return strings.HasPrefix(e.ID, filterValue)
+ }, nil
+ case "TYPE":
+ return func(e *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 *Event) bool {
+ return func(e *Event) bool {
+ return e.Time.After(timeSince)
+ }
+}
+
+func generateEventUntilOption(timeUntil time.Time) func(e *Event) bool {
+ return func(e *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) ([]EventFilter, error) {
+ var options []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
+}
diff --git a/libpod/events/journal_linux.go b/libpod/events/journal_linux.go
new file mode 100644
index 000000000..e6b54db1d
--- /dev/null
+++ b/libpod/events/journal_linux.go
@@ -0,0 +1,131 @@
+package events
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/coreos/go-systemd/journal"
+ "github.com/coreos/go-systemd/sdjournal"
+ "github.com/pkg/errors"
+)
+
+// EventJournalD is the journald implementation of an eventer
+type EventJournalD struct {
+ options EventerOptions
+}
+
+// Write to journald
+func (e EventJournalD) Write(ee Event) error {
+ m := make(map[string]string)
+ m["SYSLOG_IDENTIFIER"] = "podman"
+ m["PODMAN_EVENT"] = ee.Status.String()
+ m["PODMAN_TYPE"] = ee.Type.String()
+ m["PODMAN_TIME"] = ee.Time.Format(time.RFC3339Nano)
+
+ // Add specialized information based on the podman type
+ switch ee.Type {
+ case Image:
+ m["PODMAN_NAME"] = ee.Name
+ m["PODMAN_ID"] = ee.ID
+ case Container, Pod:
+ m["PODMAN_IMAGE"] = ee.Image
+ m["PODMAN_NAME"] = ee.Name
+ m["PODMAN_ID"] = ee.ID
+ case Volume:
+ m["PODMAN_NAME"] = ee.Name
+ }
+ return journal.Send(fmt.Sprintf("%s", ee.ToHumanReadable()), journal.PriInfo, m)
+}
+
+// Read reads events from the journal and sends qualified events to the event channel
+func (e EventJournalD) Read(options ReadOptions) error {
+ eventOptions, err := generateEventOptions(options.Filters, options.Since, options.Until)
+ if err != nil {
+ return errors.Wrapf(err, "failed to generate event options")
+ }
+ podmanJournal := sdjournal.Match{Field: "SYSLOG_IDENTIFIER", Value: "podman"} //nolint
+ j, err := sdjournal.NewJournal() //nolint
+ if err != nil {
+ return err
+ }
+ if err := j.AddMatch(podmanJournal.String()); err != nil {
+ return errors.Wrap(err, "failed to add filter for event log")
+ }
+ if len(options.Since) == 0 && len(options.Until) == 0 && options.Stream {
+ if err := j.SeekTail(); err != nil {
+ return errors.Wrap(err, "failed to seek end of journal")
+ }
+ }
+ // the api requires a next|prev before getting a cursor
+ if _, err := j.Next(); err != nil {
+ return err
+ }
+ prevCursor, err := j.GetCursor()
+ if err != nil {
+ return err
+ }
+ defer close(options.EventChannel)
+ for {
+ if _, err := j.Next(); err != nil {
+ return err
+ }
+ newCursor, err := j.GetCursor()
+ if err != nil {
+ return err
+ }
+ if prevCursor == newCursor {
+ if len(options.Until) > 0 || !options.Stream {
+ break
+ }
+ _ = j.Wait(sdjournal.IndefiniteWait) //nolint
+ continue
+ }
+ prevCursor = newCursor
+ entry, err := j.GetEntry()
+ if err != nil {
+ return err
+ }
+ newEvent, err := newEventFromJournalEntry(entry)
+ if err != nil {
+ return err
+ }
+ include := true
+ for _, filter := range eventOptions {
+ include = include && filter(newEvent)
+ }
+ if include {
+ options.EventChannel <- newEvent
+ }
+ }
+ return nil
+
+}
+
+func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { //nolint
+ newEvent := Event{}
+ eventType, err := StringToType(entry.Fields["PODMAN_TYPE"])
+ if err != nil {
+ return nil, err
+ }
+ eventTime, err := time.Parse(time.RFC3339Nano, entry.Fields["PODMAN_TIME"])
+ if err != nil {
+ return nil, err
+ }
+ eventStatus, err := StringToStatus(entry.Fields["PODMAN_EVENT"])
+ if err != nil {
+ return nil, err
+ }
+ newEvent.Type = eventType
+ newEvent.Time = eventTime
+ newEvent.Status = eventStatus
+ newEvent.Name = entry.Fields["PODMAN_NAME"]
+
+ switch eventType {
+ case Container, Pod:
+ newEvent.ID = entry.Fields["PODMAN_ID"]
+ newEvent.Image = entry.Fields["PODMAN_IMAGE"]
+ case Image:
+ newEvent.ID = entry.Fields["PODMAN_ID"]
+ }
+ return &newEvent, nil
+}
diff --git a/libpod/events/logfile.go b/libpod/events/logfile.go
new file mode 100644
index 000000000..3232b86d0
--- /dev/null
+++ b/libpod/events/logfile.go
@@ -0,0 +1,65 @@
+package events
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pkg/errors"
+)
+
+// EventLogFile is the structure for event writing to a logfile. It contains the eventer
+// options and the event itself. Methods for reading and writing are also defined from it.
+type EventLogFile struct {
+ options EventerOptions
+}
+
+// Writes to the log file
+func (e EventLogFile) Write(ee Event) error {
+ f, err := os.OpenFile(e.options.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ eventJSONString, err := ee.ToJSONString()
+ if err != nil {
+ return err
+ }
+ if _, err := f.WriteString(fmt.Sprintf("%s\n", eventJSONString)); err != nil {
+ return err
+ }
+ return nil
+
+}
+
+// Reads from the log file
+func (e EventLogFile) Read(options ReadOptions) error {
+ eventOptions, err := generateEventOptions(options.Filters, options.Since, options.Until)
+ if err != nil {
+ return errors.Wrapf(err, "unable to generate event options")
+ }
+ t, err := e.getTail(options)
+ if err != nil {
+ return err
+ }
+ for line := range t.Lines {
+ event, err := newEventFromJSONString(line.Text)
+ if err != nil {
+ return err
+ }
+ switch event.Type {
+ case Image, Volume, Pod, Container:
+ // no-op
+ default:
+ return errors.Errorf("event type %s is not valid in %s", event.Type.String(), e.options.LogFilePath)
+ }
+ include := true
+ for _, filter := range eventOptions {
+ include = include && filter(event)
+ }
+ if include {
+ options.EventChannel <- event
+ }
+ }
+ close(options.EventChannel)
+ return nil
+}
diff --git a/libpod/events/nullout.go b/libpod/events/nullout.go
new file mode 100644
index 000000000..7d811a9c7
--- /dev/null
+++ b/libpod/events/nullout.go
@@ -0,0 +1,23 @@
+package events
+
+// EventToNull is an eventer type that only performs write operations
+// and only writes to /dev/null. It is meant for unittests only
+type EventToNull struct{}
+
+// Write eats the event and always returns nil
+func (e EventToNull) Write(ee Event) error {
+ return nil
+}
+
+// Read does nothing. Do not use it.
+func (e EventToNull) Read(options ReadOptions) error {
+ return nil
+}
+
+// NewNullEventer returns a new null eventer. You should only do this for
+// the purposes on internal libpod testing.
+func NewNullEventer() Eventer {
+ var e Eventer
+ e = EventToNull{}
+ return e
+}
diff --git a/libpod/image/image.go b/libpod/image/image.go
index 757d034a2..b965a4640 100644
--- a/libpod/image/image.go
+++ b/libpod/image/image.go
@@ -66,6 +66,8 @@ type Runtime struct {
store storage.Store
SignaturePolicyPath string
EventsLogFilePath string
+ EventsLogger string
+ Eventer events.Eventer
}
// InfoImage keep information of Image along with all associated layers
@@ -353,8 +355,8 @@ func (i *Image) TopLayer() string {
// outside the context of images
// TODO: the force param does nothing as of now. Need to move container
// handling logic here eventually.
-func (i *Image) Remove(force bool) error {
- parent, err := i.GetParent()
+func (i *Image) Remove(ctx context.Context, force bool) error {
+ parent, err := i.GetParent(ctx)
if err != nil {
return err
}
@@ -363,11 +365,11 @@ func (i *Image) Remove(force bool) error {
}
i.newImageEvent(events.Remove)
for parent != nil {
- nextParent, err := parent.GetParent()
+ nextParent, err := parent.GetParent(ctx)
if err != nil {
return err
}
- children, err := parent.GetChildren()
+ children, err := parent.GetChildren(ctx)
if err != nil {
return err
}
@@ -679,7 +681,8 @@ type History struct {
Comment string `json:"comment"`
}
-// History gets the history of an image and information about its layers
+// History gets the history of an image and the IDs of images that are part of
+// its history
func (i *Image) History(ctx context.Context) ([]*History, error) {
img, err := i.toImageRef(ctx)
if err != nil {
@@ -690,31 +693,92 @@ func (i *Image) History(ctx context.Context) ([]*History, error) {
return nil, err
}
- // Get the IDs of the images making up the history layers
- // if the images exist locally in the store
+ // Use our layers list to find images that use one of them as its
+ // topmost layer.
+ interestingLayers := make(map[string]bool)
+ layer, err := i.imageruntime.store.Layer(i.TopLayer())
+ if err != nil {
+ return nil, err
+ }
+ for layer != nil {
+ interestingLayers[layer.ID] = true
+ if layer.Parent == "" {
+ break
+ }
+ layer, err = i.imageruntime.store.Layer(layer.Parent)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Get the IDs of the images that share some of our layers. Hopefully
+ // this step means that we'll be able to avoid reading the
+ // configuration of every single image in local storage later on.
images, err := i.imageruntime.GetImages()
if err != nil {
return nil, errors.Wrapf(err, "error getting images from store")
}
- imageIDs := []string{i.ID()}
- if err := i.historyLayerIDs(i.TopLayer(), images, &imageIDs); err != nil {
- return nil, errors.Wrap(err, "error getting image IDs for layers in history")
+ interestingImages := make([]*Image, 0, len(images))
+ for i := range images {
+ if interestingLayers[images[i].TopLayer()] {
+ interestingImages = append(interestingImages, images[i])
+ }
+ }
+
+ // Build a list of image IDs that correspond to our history entries.
+ historyImages := make([]*Image, len(oci.History))
+ if len(oci.History) > 0 {
+ // The starting image shares its whole history with itself.
+ historyImages[len(historyImages)-1] = i
+ for i := range interestingImages {
+ image, err := images[i].ociv1Image(ctx)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error getting image configuration for image %q", images[i].ID())
+ }
+ // If the candidate has a longer history or no history
+ // at all, then it doesn't share the portion of our
+ // history that we're interested in matching with other
+ // images.
+ if len(image.History) == 0 || len(image.History) > len(historyImages) {
+ continue
+ }
+ // If we don't include all of the layers that the
+ // candidate image does (i.e., our rootfs didn't look
+ // like its rootfs at any point), then it can't be part
+ // of our history.
+ if len(image.RootFS.DiffIDs) > len(oci.RootFS.DiffIDs) {
+ continue
+ }
+ candidateLayersAreUsed := true
+ for i := range image.RootFS.DiffIDs {
+ if image.RootFS.DiffIDs[i] != oci.RootFS.DiffIDs[i] {
+ candidateLayersAreUsed = false
+ break
+ }
+ }
+ if !candidateLayersAreUsed {
+ continue
+ }
+ // If the candidate's entire history is an initial
+ // portion of our history, then we're based on it,
+ // either directly or indirectly.
+ sharedHistory := historiesMatch(oci.History, image.History)
+ if sharedHistory == len(image.History) {
+ historyImages[sharedHistory-1] = images[i]
+ }
+ }
}
var (
- imageID string
- imgIDCount = 0
size int64
sizeCount = 1
allHistory []*History
)
for i := len(oci.History) - 1; i >= 0; i-- {
- if imgIDCount < len(imageIDs) {
- imageID = imageIDs[imgIDCount]
- imgIDCount++
- } else {
- imageID = "<missing>"
+ imageID := "<missing>"
+ if historyImages[i] != nil {
+ imageID = historyImages[i].ID()
}
if !oci.History[i].EmptyLayer {
size = img.LayerInfos()[len(img.LayerInfos())-sizeCount].Size
@@ -1006,26 +1070,110 @@ func splitString(input string) string {
// IsParent goes through the layers in the store and checks if i.TopLayer is
// the parent of any other layer in store. Double check that image with that
// layer exists as well.
-func (i *Image) IsParent() (bool, error) {
- children, err := i.GetChildren()
+func (i *Image) IsParent(ctx context.Context) (bool, error) {
+ children, err := i.getChildren(ctx, 1)
if err != nil {
return false, err
}
return len(children) > 0, nil
}
+// historiesMatch returns the number of entries in the histories which have the
+// same contents
+func historiesMatch(a, b []imgspecv1.History) int {
+ i := 0
+ for i < len(a) && i < len(b) {
+ if a[i].Created != nil && b[i].Created == nil {
+ return i
+ }
+ if a[i].Created == nil && b[i].Created != nil {
+ return i
+ }
+ if a[i].Created != nil && b[i].Created != nil {
+ if !a[i].Created.Equal(*(b[i].Created)) {
+ return i
+ }
+ }
+ if a[i].CreatedBy != b[i].CreatedBy {
+ return i
+ }
+ if a[i].Author != b[i].Author {
+ return i
+ }
+ if a[i].Comment != b[i].Comment {
+ return i
+ }
+ if a[i].EmptyLayer != b[i].EmptyLayer {
+ return i
+ }
+ i++
+ }
+ return i
+}
+
+// areParentAndChild checks diff ID and history in the two images and return
+// true if the second should be considered to be directly based on the first
+func areParentAndChild(parent, child *imgspecv1.Image) bool {
+ // the child and candidate parent should share all of the
+ // candidate parent's diff IDs, which together would have
+ // controlled which layers were used
+ if len(parent.RootFS.DiffIDs) > len(child.RootFS.DiffIDs) {
+ return false
+ }
+ childUsesCandidateDiffs := true
+ for i := range parent.RootFS.DiffIDs {
+ if child.RootFS.DiffIDs[i] != parent.RootFS.DiffIDs[i] {
+ childUsesCandidateDiffs = false
+ break
+ }
+ }
+ if !childUsesCandidateDiffs {
+ return false
+ }
+ // the child should have the same history as the parent, plus
+ // one more entry
+ if len(parent.History)+1 != len(child.History) {
+ return false
+ }
+ if historiesMatch(parent.History, child.History) != len(parent.History) {
+ return false
+ }
+ return true
+}
+
// GetParent returns the image ID of the parent. Return nil if a parent is not found.
-func (i *Image) GetParent() (*Image, error) {
+func (i *Image) GetParent(ctx context.Context) (*Image, error) {
images, err := i.imageruntime.GetImages()
if err != nil {
return nil, err
}
- layer, err := i.imageruntime.store.Layer(i.TopLayer())
+ childLayer, err := i.imageruntime.store.Layer(i.TopLayer())
+ if err != nil {
+ return nil, err
+ }
+ // fetch the configuration for the child image
+ child, err := i.ociv1Image(ctx)
if err != nil {
return nil, err
}
for _, img := range images {
- if img.TopLayer() == layer.Parent {
+ if img.ID() == i.ID() {
+ continue
+ }
+ candidateLayer := img.TopLayer()
+ // as a child, our top layer is either the candidate parent's
+ // layer, or one that's derived from it, so skip over any
+ // candidate image where we know that isn't the case
+ if candidateLayer != childLayer.Parent && candidateLayer != childLayer.ID {
+ continue
+ }
+ // fetch the configuration for the candidate image
+ candidate, err := img.ociv1Image(ctx)
+ if err != nil {
+ return nil, err
+ }
+ // compare them
+ if areParentAndChild(candidate, child) {
return img, nil
}
}
@@ -1033,36 +1181,53 @@ func (i *Image) GetParent() (*Image, error) {
}
// GetChildren returns a list of the imageIDs that depend on the image
-func (i *Image) GetChildren() ([]string, error) {
+func (i *Image) GetChildren(ctx context.Context) ([]string, error) {
+ return i.getChildren(ctx, 0)
+}
+
+// getChildren returns a list of at most "max" imageIDs that depend on the image
+func (i *Image) getChildren(ctx context.Context, max int) ([]string, error) {
var children []string
images, err := i.imageruntime.GetImages()
if err != nil {
return nil, err
}
- layers, err := i.imageruntime.store.Layers()
+
+ // fetch the configuration for the parent image
+ parent, err := i.ociv1Image(ctx)
if err != nil {
return nil, err
}
+ parentLayer := i.TopLayer()
- for _, layer := range layers {
- if layer.Parent == i.TopLayer() {
- if imageID := getImageOfTopLayer(images, layer.ID); len(imageID) > 0 {
- children = append(children, imageID...)
- }
- }
- }
- return children, nil
-}
-
-// getImageOfTopLayer returns the image ID where layer is the top layer of the image
-func getImageOfTopLayer(images []*Image, layer string) []string {
- var matches []string
for _, img := range images {
- if img.TopLayer() == layer {
- matches = append(matches, img.ID())
+ if img.ID() == i.ID() {
+ continue
+ }
+ candidateLayer, err := img.Layer()
+ if err != nil {
+ return nil, err
+ }
+ // if this image's top layer is not our top layer, and is not
+ // based on our top layer, we can skip it
+ if candidateLayer.Parent != parentLayer && candidateLayer.ID != parentLayer {
+ continue
+ }
+ // fetch the configuration for the candidate image
+ candidate, err := img.ociv1Image(ctx)
+ if err != nil {
+ return nil, err
+ }
+ // compare them
+ if areParentAndChild(parent, candidate) {
+ children = append(children, img.ID())
+ }
+ // if we're not building an exhaustive list, maybe we're done?
+ if max > 0 && len(children) >= max {
+ break
}
}
- return matches
+ return children, nil
}
// InputIsID returns a bool if the user input for an image
@@ -1203,7 +1368,7 @@ 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 {
+ if err := ir.Eventer.Write(e); err != nil {
logrus.Infof("unable to write event to %s", ir.EventsLogFilePath)
}
}
@@ -1216,7 +1381,7 @@ func (i *Image) newImageEvent(status events.Status) {
if len(i.Names()) > 0 {
e.Name = i.Names()[0]
}
- if err := e.Write(i.imageruntime.EventsLogFilePath); err != nil {
+ if err := i.imageruntime.Eventer.Write(e); err != nil {
logrus.Infof("unable to write event to %s", i.imageruntime.EventsLogFilePath)
}
}
diff --git a/libpod/image/image_test.go b/libpod/image/image_test.go
index 075ba119d..e93ebf797 100644
--- a/libpod/image/image_test.go
+++ b/libpod/image/image_test.go
@@ -3,6 +3,7 @@ package image
import (
"context"
"fmt"
+ "github.com/containers/libpod/libpod/events"
"io"
"io/ioutil"
"os"
@@ -87,6 +88,7 @@ func TestImage_NewFromLocal(t *testing.T) {
// Need images to be present for this test
ir, err := NewImageRuntimeFromOptions(so)
assert.NoError(t, err)
+ ir.Eventer = events.NewNullEventer()
bb, err := ir.New(context.Background(), "docker.io/library/busybox:latest", "", "", writer, nil, SigningOptions{}, false, nil)
assert.NoError(t, err)
bbglibc, err := ir.New(context.Background(), "docker.io/library/busybox:glibc", "", "", writer, nil, SigningOptions{}, false, nil)
@@ -127,6 +129,7 @@ func TestImage_New(t *testing.T) {
}
ir, err := NewImageRuntimeFromOptions(so)
assert.NoError(t, err)
+ ir.Eventer = events.NewNullEventer()
// Build the list of pull names
names = append(names, bbNames...)
names = append(names, fedoraNames...)
@@ -139,7 +142,7 @@ func TestImage_New(t *testing.T) {
newImage, err := ir.New(context.Background(), img, "", "", writer, nil, SigningOptions{}, false, nil)
assert.NoError(t, err)
assert.NotEqual(t, newImage.ID(), "")
- err = newImage.Remove(false)
+ err = newImage.Remove(context.Background(), false)
assert.NoError(t, err)
}
@@ -164,6 +167,7 @@ func TestImage_MatchRepoTag(t *testing.T) {
}
ir, err := NewImageRuntimeFromOptions(so)
assert.NoError(t, err)
+ ir.Eventer = events.NewNullEventer()
newImage, err := ir.New(context.Background(), "busybox", "", "", os.Stdout, nil, SigningOptions{}, false, nil)
assert.NoError(t, err)
err = newImage.TagImage("foo:latest")
diff --git a/libpod/image/prune.go b/libpod/image/prune.go
index 5bd3c2c99..a4f8a0c9f 100644
--- a/libpod/image/prune.go
+++ b/libpod/image/prune.go
@@ -1,6 +1,8 @@
package image
import (
+ "context"
+
"github.com/containers/libpod/libpod/events"
"github.com/pkg/errors"
)
@@ -34,14 +36,14 @@ func (ir *Runtime) GetPruneImages(all bool) ([]*Image, error) {
// PruneImages prunes dangling and optionally all unused images from the local
// image store
-func (ir *Runtime) PruneImages(all bool) ([]string, error) {
+func (ir *Runtime) PruneImages(ctx context.Context, all bool) ([]string, error) {
var prunedCids []string
pruneImages, err := ir.GetPruneImages(all)
if err != nil {
return nil, errors.Wrap(err, "unable to get images to prune")
}
for _, p := range pruneImages {
- if err := p.Remove(true); err != nil {
+ if err := p.Remove(ctx, true); err != nil {
return nil, errors.Wrap(err, "failed to prune image")
}
defer p.newImageEvent(events.Prune)
diff --git a/libpod/runtime.go b/libpod/runtime.go
index 3b1c2be98..d03731284 100644
--- a/libpod/runtime.go
+++ b/libpod/runtime.go
@@ -2,6 +2,7 @@ package libpod
import (
"fmt"
+ "github.com/containers/libpod/libpod/events"
"io/ioutil"
"os"
"path/filepath"
@@ -105,6 +106,9 @@ type Runtime struct {
// storage unusable). When valid is false, the runtime cannot be used.
valid bool
lock sync.RWMutex
+
+ // mechanism to read and write even logs
+ eventer events.Eventer
}
// OCIRuntimePath contains information about an OCI runtime.
@@ -222,6 +226,8 @@ type RuntimeConfig struct {
// pods.
NumLocks uint32 `toml:"num_locks,omitempty"`
+ // EventsLogger determines where events should be logged
+ EventsLogger string `toml:"events_logger"`
// EventsLogFilePath is where the events log is stored.
EventsLogFilePath string `toml:-"events_logfile_path"`
}
@@ -252,7 +258,6 @@ func defaultRuntimeConfig() (RuntimeConfig, error) {
if err != nil {
return RuntimeConfig{}, err
}
-
return RuntimeConfig{
// Leave this empty so containers/storage will use its defaults
StorageConfig: storage.StoreOptions{},
@@ -296,6 +301,7 @@ func defaultRuntimeConfig() (RuntimeConfig, error) {
EnablePortReservation: true,
EnableLabeling: true,
NumLocks: 2048,
+ EventsLogger: "journald",
}, nil
}
@@ -755,16 +761,24 @@ func makeRuntime(runtime *Runtime) (err error) {
// Set up image runtime and store in runtime
ir := image.NewImageRuntimeFromStore(runtime.store)
- if err != nil {
- return err
- }
runtime.imageRuntime = ir
// Setting signaturepolicypath
ir.SignaturePolicyPath = runtime.config.SignaturePolicyPath
+
// Set logfile path for events
ir.EventsLogFilePath = runtime.config.EventsLogFilePath
+ // Set logger type
+ ir.EventsLogger = runtime.config.EventsLogger
+
+ // Setup the eventer
+ eventer, err := runtime.newEventer()
+ if err != nil {
+ return err
+ }
+ runtime.eventer = eventer
+ ir.Eventer = eventer
defer func() {
if err != nil && store != nil {
@@ -1018,6 +1032,8 @@ func (r *Runtime) Shutdown(force bool) error {
// Refreshes the state, recreating temporary files
// Does not check validity as the runtime is not valid until after this has run
func (r *Runtime) refresh(alivePath string) error {
+ logrus.Debugf("Podman detected system restart - performing state refresh")
+
// First clear the state in the database
if err := r.state.Refresh(); err != nil {
return err
diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go
index 02f925fc6..5e9f65acc 100644
--- a/libpod/runtime_img.go
+++ b/libpod/runtime_img.go
@@ -57,7 +57,7 @@ func (r *Runtime) RemoveImage(ctx context.Context, img *image.Image, force bool)
}
}
- hasChildren, err := img.IsParent()
+ hasChildren, err := img.IsParent(ctx)
if err != nil {
return "", err
}
@@ -82,12 +82,12 @@ func (r *Runtime) RemoveImage(ctx context.Context, img *image.Image, force bool)
// reponames and no force is applied, we error out.
return "", fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", img.ID())
}
- err = img.Remove(force)
+ err = img.Remove(ctx, force)
if err != nil && errors.Cause(err) == storage.ErrImageUsedByContainer {
if errStorage := r.rmStorageContainers(force, img); errStorage == nil {
// Containers associated with the image should be deleted now,
// let's try removing the image again.
- err = img.Remove(force)
+ err = img.Remove(ctx, force)
} else {
err = errStorage
}