summaryrefslogtreecommitdiff
path: root/libpod
diff options
context:
space:
mode:
Diffstat (limited to 'libpod')
-rw-r--r--libpod/boltdb_state.go167
-rw-r--r--libpod/container.go15
-rw-r--r--libpod/container_api.go38
-rw-r--r--libpod/container_commit.go2
-rw-r--r--libpod/container_inspect.go12
-rw-r--r--libpod/container_internal.go87
-rw-r--r--libpod/container_internal_linux.go136
-rw-r--r--libpod/container_internal_test.go48
-rw-r--r--libpod/container_internal_unsupported.go4
-rw-r--r--libpod/container_log.go213
-rw-r--r--libpod/events.go96
-rw-r--r--libpod/events/events.go268
-rw-r--r--libpod/healthcheck.go320
-rw-r--r--libpod/image/image.go141
-rw-r--r--libpod/image/prune.go6
-rw-r--r--libpod/image/pull.go7
-rw-r--r--libpod/networking_linux.go48
-rw-r--r--libpod/oci.go41
-rw-r--r--libpod/oci_linux.go17
-rw-r--r--libpod/options.go27
-rw-r--r--libpod/pod_api.go13
-rw-r--r--libpod/runtime.go191
-rw-r--r--libpod/runtime_ctr.go11
-rw-r--r--libpod/runtime_img.go29
-rw-r--r--libpod/runtime_pod_infra_linux.go46
-rw-r--r--libpod/runtime_pod_linux.go10
-rw-r--r--libpod/runtime_volume.go6
-rw-r--r--libpod/runtime_volume_linux.go5
-rw-r--r--libpod/runtime_volume_unsupported.go2
29 files changed, 1770 insertions, 236 deletions
diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go
index c226a0617..92a7b1538 100644
--- a/libpod/boltdb_state.go
+++ b/libpod/boltdb_state.go
@@ -382,6 +382,11 @@ func (s *BoltState) LookupContainer(idOrName string) (*Container, error) {
return err
}
+ namesBucket, err := getNamesBucket(tx)
+ if err != nil {
+ return err
+ }
+
nsBucket, err := getNSBucket(tx)
if err != nil {
return err
@@ -395,41 +400,59 @@ func (s *BoltState) LookupContainer(idOrName string) (*Container, error) {
// It might not be in our namespace, but
// getContainerFromDB() will handle that case.
id = []byte(idOrName)
- } else {
- // They did not give us a full container ID.
- // Search for partial ID or full name matches
- // Use else-if in case the name is set to a partial ID
- exists := false
- err = idBucket.ForEach(func(checkID, checkName []byte) error {
- // If the container isn't in our namespace, we
- // can't match it
- if s.namespaceBytes != nil {
- ns := nsBucket.Get(checkID)
- if !bytes.Equal(ns, s.namespaceBytes) {
- return nil
- }
+ return s.getContainerFromDB(id, ctr, ctrBucket)
+ }
+
+ // Next, check if the full name was given
+ isPod := false
+ fullID := namesBucket.Get([]byte(idOrName))
+ if fullID != nil {
+ // The name exists and maps to an ID.
+ // However, we are not yet certain the ID is a
+ // container.
+ ctrExists = ctrBucket.Bucket(fullID)
+ if ctrExists != nil {
+ // A container bucket matching the full ID was
+ // found.
+ return s.getContainerFromDB(fullID, ctr, ctrBucket)
+ }
+ // Don't error if we have a name match but it's not a
+ // container - there's a chance we have a container with
+ // an ID starting with those characters.
+ // However, so we can return a good error, note whether
+ // this is a pod.
+ isPod = true
+ }
+
+ // We were not given a full container ID or name.
+ // Search for partial ID matches.
+ exists := false
+ err = idBucket.ForEach(func(checkID, checkName []byte) error {
+ // If the container isn't in our namespace, we
+ // can't match it
+ if s.namespaceBytes != nil {
+ ns := nsBucket.Get(checkID)
+ if !bytes.Equal(ns, s.namespaceBytes) {
+ return nil
}
- if string(checkName) == idOrName {
- if exists {
- return errors.Wrapf(ErrCtrExists, "more than one result for ID or name %s", idOrName)
- }
- id = checkID
- exists = true
- } else if strings.HasPrefix(string(checkID), idOrName) {
- if exists {
- return errors.Wrapf(ErrCtrExists, "more than one result for ID or name %s", idOrName)
- }
- id = checkID
- exists = true
+ }
+ if strings.HasPrefix(string(checkID), idOrName) {
+ if exists {
+ return errors.Wrapf(ErrCtrExists, "more than one result for container ID %s", idOrName)
}
+ id = checkID
+ exists = true
+ }
- return nil
- })
- if err != nil {
- return err
- } else if !exists {
- return errors.Wrapf(ErrNoSuchCtr, "no container with name or ID %s found", idOrName)
+ return nil
+ })
+ if err != nil {
+ return err
+ } else if !exists {
+ if isPod {
+ return errors.Wrapf(ErrNoSuchCtr, "%s is a pod, not a container", idOrName)
}
+ return errors.Wrapf(ErrNoSuchCtr, "no container with name or ID %s found", idOrName)
}
return s.getContainerFromDB(id, ctr, ctrBucket)
@@ -941,6 +964,11 @@ func (s *BoltState) LookupPod(idOrName string) (*Pod, error) {
return err
}
+ namesBkt, err := getNamesBucket(tx)
+ if err != nil {
+ return err
+ }
+
nsBkt, err := getNSBucket(tx)
if err != nil {
return err
@@ -954,41 +982,56 @@ func (s *BoltState) LookupPod(idOrName string) (*Pod, error) {
// It might not be in our namespace, but getPodFromDB()
// will handle that case.
id = []byte(idOrName)
- } else {
- // They did not give us a full pod ID.
- // Search for partial ID or full name matches
- // Use else-if in case the name is set to a partial ID
- exists := false
- err = idBucket.ForEach(func(checkID, checkName []byte) error {
- // If the pod isn't in our namespace, we
- // can't match it
- if s.namespaceBytes != nil {
- ns := nsBkt.Get(checkID)
- if !bytes.Equal(ns, s.namespaceBytes) {
- return nil
- }
+ return s.getPodFromDB(id, pod, podBkt)
+ }
+
+ // Next, check if the full name was given
+ isCtr := false
+ fullID := namesBkt.Get([]byte(idOrName))
+ if fullID != nil {
+ // The name exists and maps to an ID.
+ // However, we aren't yet sure if the ID is a pod.
+ podExists = podBkt.Bucket(fullID)
+ if podExists != nil {
+ // A pod bucket matching the full ID was found.
+ return s.getPodFromDB(fullID, pod, podBkt)
+ }
+ // Don't error if we have a name match but it's not a
+ // pod - there's a chance we have a pod with an ID
+ // starting with those characters.
+ // However, so we can return a good error, note whether
+ // this is a container.
+ isCtr = true
+ }
+ // They did not give us a full pod name or ID.
+ // Search for partial ID matches.
+ exists := false
+ err = idBucket.ForEach(func(checkID, checkName []byte) error {
+ // If the pod isn't in our namespace, we
+ // can't match it
+ if s.namespaceBytes != nil {
+ ns := nsBkt.Get(checkID)
+ if !bytes.Equal(ns, s.namespaceBytes) {
+ return nil
}
- if string(checkName) == idOrName {
- if exists {
- return errors.Wrapf(ErrPodExists, "more than one result for ID or name %s", idOrName)
- }
- id = checkID
- exists = true
- } else if strings.HasPrefix(string(checkID), idOrName) {
- if exists {
- return errors.Wrapf(ErrPodExists, "more than one result for ID or name %s", idOrName)
- }
- id = checkID
- exists = true
+ }
+ if strings.HasPrefix(string(checkID), idOrName) {
+ if exists {
+ return errors.Wrapf(ErrPodExists, "more than one result for ID or name %s", idOrName)
}
+ id = checkID
+ exists = true
+ }
- return nil
- })
- if err != nil {
- return err
- } else if !exists {
- return errors.Wrapf(ErrNoSuchPod, "no pod with name or ID %s found", idOrName)
+ return nil
+ })
+ if err != nil {
+ return err
+ } else if !exists {
+ if isCtr {
+ return errors.Wrapf(ErrNoSuchPod, "%s is a container, not a pod", idOrName)
}
+ return errors.Wrapf(ErrNoSuchPod, "no pod with name or ID %s found", idOrName)
}
// We might have found a container ID, but it's OK
diff --git a/libpod/container.go b/libpod/container.go
index 75f4a4a4f..ec4e31026 100644
--- a/libpod/container.go
+++ b/libpod/container.go
@@ -10,6 +10,7 @@ import (
"github.com/containernetworking/cni/pkg/types"
cnitypes "github.com/containernetworking/cni/pkg/types/current"
+ "github.com/containers/image/manifest"
"github.com/containers/libpod/libpod/lock"
"github.com/containers/libpod/pkg/namespaces"
"github.com/containers/storage"
@@ -365,6 +366,9 @@ type ContainerConfig struct {
// Systemd tells libpod to setup the container in systemd mode
Systemd bool `json:"systemd"`
+
+ // HealtchCheckConfig has the health check command and related timings
+ HealthCheckConfig *manifest.Schema2HealthConfig `json:"healthcheck"`
}
// ContainerStatus returns a string representation for users
@@ -1085,3 +1089,14 @@ func (c *Container) ContainerState() (*ContainerState, error) {
deepcopier.Copy(c.state).To(returnConfig)
return c.state, nil
}
+
+// HasHealthCheck returns bool as to whether there is a health check
+// defined for the container
+func (c *Container) HasHealthCheck() bool {
+ return c.config.HealthCheckConfig != nil
+}
+
+// HealthCheckConfig returns the command and timing attributes of the health check
+func (c *Container) HealthCheckConfig() *manifest.Schema2HealthConfig {
+ return c.config.HealthCheckConfig
+}
diff --git a/libpod/container_api.go b/libpod/container_api.go
index 6bef3c47d..96435c2ff 100644
--- a/libpod/container_api.go
+++ b/libpod/container_api.go
@@ -10,10 +10,11 @@ import (
"time"
"github.com/containers/libpod/libpod/driver"
+ "github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/inspect"
"github.com/containers/libpod/pkg/lookup"
"github.com/containers/storage/pkg/stringid"
- "github.com/docker/docker/daemon/caps"
+ "github.com/docker/docker/oci/caps"
opentracing "github.com/opentracing/opentracing-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -125,7 +126,7 @@ func (c *Container) StartAndAttach(ctx context.Context, streams *AttachStreams,
}
close(attachChan)
}()
-
+ c.newContainerEvent(events.Attach)
return attachChan, nil
}
@@ -180,7 +181,7 @@ func (c *Container) StopWithTimeout(timeout uint) error {
c.state.State == ContainerStateExited {
return ErrCtrStopped
}
-
+ defer c.newContainerEvent(events.Stop)
return c.stop(timeout)
}
@@ -198,13 +199,13 @@ func (c *Container) Kill(signal uint) error {
if c.state.State != ContainerStateRunning {
return errors.Wrapf(ErrCtrStateInvalid, "can only kill running containers")
}
-
+ defer c.newContainerEvent(events.Kill)
return c.runtime.ociRuntime.killContainer(c, signal)
}
// Exec starts a new process inside the container
// TODO investigate allowing exec without attaching
-func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir string, streams *AttachStreams) error {
+func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir string, streams *AttachStreams, preserveFDs int) error {
var capList []string
locked := false
@@ -266,7 +267,7 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir
logrus.Debugf("Creating new exec session in container %s with session id %s", c.ID(), sessionID)
- execCmd, err := c.runtime.ociRuntime.execContainer(c, cmd, capList, env, tty, workDir, hostUser, sessionID, streams)
+ execCmd, err := c.runtime.ociRuntime.execContainer(c, cmd, capList, env, tty, workDir, hostUser, sessionID, streams, preserveFDs)
if err != nil {
return errors.Wrapf(err, "error exec %s", c.ID())
}
@@ -321,7 +322,7 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir
// TODO handle this better
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())
// Unlock so other processes can use the container
@@ -351,7 +352,6 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir
if err := c.save(); err != nil {
logrus.Errorf("Error removing exec session %s from container %s state: %v", sessionID, c.ID(), err)
}
-
return waitErr
}
@@ -390,7 +390,7 @@ func (c *Container) Attach(streams *AttachStreams, keys string, resize <-chan re
c.state.State != ContainerStateExited {
return errors.Wrapf(ErrCtrStateInvalid, "can only attach to created or running containers")
}
-
+ defer c.newContainerEvent(events.Attach)
return c.attach(streams, keys, resize, false)
}
@@ -405,7 +405,7 @@ func (c *Container) Mount() (string, error) {
return "", err
}
}
-
+ defer c.newContainerEvent(events.Mount)
return c.mount()
}
@@ -435,6 +435,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())
}
}
+ defer c.newContainerEvent(events.Unmount)
return c.unmount(force)
}
@@ -455,7 +456,7 @@ func (c *Container) Pause() error {
if c.state.State != ContainerStateRunning {
return errors.Wrapf(ErrCtrStateInvalid, "%q is not running, can't pause", c.state.State)
}
-
+ defer c.newContainerEvent(events.Pause)
return c.pause()
}
@@ -473,7 +474,7 @@ func (c *Container) Unpause() error {
if c.state.State != ContainerStatePaused {
return errors.Wrapf(ErrCtrStateInvalid, "%q is not paused, can't unpause", c.ID())
}
-
+ defer c.newContainerEvent(events.Unpause)
return c.unpause()
}
@@ -488,7 +489,7 @@ func (c *Container) Export(path string) error {
return err
}
}
-
+ defer c.newContainerEvent(events.Export)
return c.export(path)
}
@@ -542,7 +543,6 @@ func (c *Container) Inspect(size bool) (*inspect.ContainerInspectData, error) {
if err != nil {
return nil, errors.Wrapf(err, "error getting graph driver info %q", c.ID())
}
-
return c.getContainerInspectData(size, driverData)
}
@@ -574,6 +574,7 @@ func (c *Container) WaitWithInterval(waitTimeout time.Duration) (int32, error) {
return 0, err
}
exitCode := c.state.ExitCode
+ c.newContainerEvent(events.Wait)
return exitCode, nil
}
@@ -597,7 +598,7 @@ func (c *Container) Cleanup(ctx context.Context) error {
if len(c.state.ExecSessions) != 0 {
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)
}
@@ -667,7 +668,7 @@ func (c *Container) Sync() error {
}
}
}
-
+ defer c.newContainerEvent(events.Sync)
return nil
}
@@ -772,7 +773,6 @@ func (c *Container) Refresh(ctx context.Context) error {
return err
}
}
-
return nil
}
@@ -800,7 +800,7 @@ func (c *Container) Checkpoint(ctx context.Context, options ContainerCheckpointO
return err
}
}
-
+ defer c.newContainerEvent(events.Checkpoint)
return c.checkpoint(ctx, options)
}
@@ -815,6 +815,6 @@ func (c *Container) Restore(ctx context.Context, options ContainerCheckpointOpti
return err
}
}
-
+ defer c.newContainerEvent(events.Restore)
return c.restore(ctx, options)
}
diff --git a/libpod/container_commit.go b/libpod/container_commit.go
index 5c4fd1a31..0604a550b 100644
--- a/libpod/container_commit.go
+++ b/libpod/container_commit.go
@@ -8,6 +8,7 @@ import (
"github.com/containers/buildah"
"github.com/containers/buildah/util"
is "github.com/containers/image/storage"
+ "github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/libpod/image"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -177,5 +178,6 @@ func (c *Container) Commit(ctx context.Context, destImage string, options Contai
if err != nil {
return nil, err
}
+ defer c.newContainerEvent(events.Commit)
return c.runtime.imageRuntime.NewFromLocal(id)
}
diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go
index e2730c282..aa3a07888 100644
--- a/libpod/container_inspect.go
+++ b/libpod/container_inspect.go
@@ -93,6 +93,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
HostsPath: hostsPath,
StaticDir: config.StaticDir,
LogPath: config.LogPath,
+ ConmonPidFile: config.ConmonPidFile,
Name: config.Name,
Driver: driverData.Name,
MountLabel: config.MountLabel,
@@ -127,6 +128,17 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
IsInfra: c.IsInfra(),
}
+ if c.config.HealthCheckConfig != nil {
+ // This container has a healthcheck defined in it; we need to add it's state
+ healthCheckState, err := c.GetHealthCheckLog()
+ if err != nil {
+ // An error here is not considered fatal; no health state will be displayed
+ logrus.Error(err)
+ } else {
+ data.State.Healthcheck = healthCheckState
+ }
+ }
+
// Copy port mappings into network settings
if config.PortMappings != nil {
data.NetworkSettings.Ports = config.PortMappings
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index e3753d825..7a90bc7d4 100644
--- a/libpod/container_internal.go
+++ b/libpod/container_internal.go
@@ -12,6 +12,7 @@ import (
"strings"
"time"
+ "github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/ctime"
"github.com/containers/libpod/pkg/hooks"
"github.com/containers/libpod/pkg/hooks/exec"
@@ -34,8 +35,8 @@ const (
)
var (
- // localeToLanguage maps from locale values to language tags.
- localeToLanguage = map[string]string{
+ // localeToLanguageMap maps from locale values to language tags.
+ localeToLanguageMap = map[string]string{
"": "und-u-va-posix",
"c": "und-u-va-posix",
"posix": "und-u-va-posix",
@@ -50,6 +51,9 @@ func (c *Container) rootFsSize() (int64, error) {
if c.config.Rootfs != "" {
return 0, nil
}
+ if c.runtime.store == nil {
+ return 0, nil
+ }
container, err := c.runtime.store.Container(c.ID())
if err != nil {
@@ -210,6 +214,9 @@ func (c *Container) handleExitFile(exitFile string, fi os.FileInfo) error {
c.state.Exited = true
+ // Write an event for the container's death
+ c.newContainerExitedEvent(c.state.ExitCode)
+
return nil
}
@@ -333,11 +340,13 @@ func (c *Container) setupStorage(ctx context.Context) error {
}
// Set the default Entrypoint and Command
- if c.config.Entrypoint == nil {
- c.config.Entrypoint = containerInfo.Config.Config.Entrypoint
- }
- if c.config.Command == nil {
- c.config.Command = containerInfo.Config.Config.Cmd
+ if containerInfo.Config != nil {
+ if c.config.Entrypoint == nil {
+ c.config.Entrypoint = containerInfo.Config.Config.Entrypoint
+ }
+ if c.config.Command == nil {
+ c.config.Command = containerInfo.Config.Config.Cmd
+ }
}
artifacts := filepath.Join(c.config.StaticDir, artifactsDir)
@@ -824,7 +833,13 @@ func (c *Container) init(ctx context.Context) error {
if err := c.save(); err != nil {
return err
}
+ if c.config.HealthCheckConfig != nil {
+ if err := c.createTimer(); err != nil {
+ logrus.Error(err)
+ }
+ }
+ defer c.newContainerEvent(events.Init)
return c.completeNetworkSetup()
}
@@ -947,6 +962,17 @@ func (c *Container) start() error {
c.state.State = ContainerStateRunning
+ if c.config.HealthCheckConfig != nil {
+ if err := c.updateHealthStatus(HealthCheckStarting); err != nil {
+ logrus.Error(err)
+ }
+ if err := c.startTimer(); err != nil {
+ logrus.Error(err)
+ }
+ }
+
+ defer c.newContainerEvent(events.Start)
+
return c.save()
}
@@ -1022,7 +1048,6 @@ func (c *Container) restartWithTimeout(ctx context.Context, timeout uint) (err e
return err
}
}
-
return c.start()
}
@@ -1086,7 +1111,7 @@ func (c *Container) cleanupStorage() error {
// error
// We still want to be able to kick the container out of the
// state
- if err == storage.ErrNotAContainer || err == storage.ErrContainerUnknown {
+ if errors.Cause(err) == storage.ErrNotAContainer || errors.Cause(err) == storage.ErrContainerUnknown {
logrus.Errorf("Storage for container %s has been removed", c.ID())
return nil
}
@@ -1113,6 +1138,13 @@ func (c *Container) cleanup(ctx context.Context) error {
logrus.Debugf("Cleaning up container %s", c.ID())
+ // Remove healthcheck unit/timer file if it execs
+ if c.config.HealthCheckConfig != nil {
+ if err := c.removeTimer(); err != nil {
+ logrus.Error(err)
+ }
+ }
+
// Clean up network namespace, if present
if err := c.cleanupNetwork(); err != nil {
lastError = err
@@ -1231,6 +1263,23 @@ func (c *Container) writeStringToRundir(destFile, output string) (string, error)
return filepath.Join(c.state.DestinationRunDir, destFile), nil
}
+// appendStringToRundir appends the provided string to the runtimedir file
+func (c *Container) appendStringToRundir(destFile, output string) (string, error) {
+ destFileName := filepath.Join(c.state.RunDir, destFile)
+
+ f, err := os.OpenFile(destFileName, os.O_APPEND|os.O_WRONLY, 0600)
+ if err != nil {
+ return "", errors.Wrapf(err, "unable to open %s", destFileName)
+ }
+ defer f.Close()
+
+ if _, err := f.WriteString(output); err != nil {
+ return "", errors.Wrapf(err, "unable to write %s", destFileName)
+ }
+
+ return filepath.Join(c.state.DestinationRunDir, destFile), nil
+}
+
// Save OCI spec to disk, replacing any existing specs for the container
func (c *Container) saveSpec(spec *spec.Spec) error {
// If the OCI spec already exists, we need to replace it
@@ -1264,6 +1313,16 @@ func (c *Container) saveSpec(spec *spec.Spec) error {
return nil
}
+// localeToLanguage translates POSIX locale strings to BCP 47 language tags.
+func localeToLanguage(locale string) string {
+ locale = strings.Replace(strings.SplitN(locale, ".", 2)[0], "_", "-", 1)
+ langString, ok := localeToLanguageMap[strings.ToLower(locale)]
+ if !ok {
+ langString = locale
+ }
+ return langString
+}
+
// Warning: precreate hooks may alter 'config' in place.
func (c *Container) setupOCIHooks(ctx context.Context, config *spec.Spec) (extensionStageHooks map[string][]spec.Hook, err error) {
var locale string
@@ -1279,11 +1338,7 @@ func (c *Container) setupOCIHooks(ctx context.Context, config *spec.Spec) (exten
}
}
- langString, ok := localeToLanguage[strings.ToLower(locale)]
- if !ok {
- langString = locale
- }
-
+ langString := localeToLanguage(locale)
lang, err := language.Parse(langString)
if err != nil {
logrus.Warnf("failed to parse language %q: %s", langString, err)
@@ -1399,5 +1454,9 @@ func (c *Container) copyWithTarFromImage(src, dest string) error {
}
a := archive.NewDefaultArchiver()
source := filepath.Join(mountpoint, src)
+
+ if err = c.copyOwnerAndPerms(source, dest); err != nil {
+ return err
+ }
return a.CopyWithTar(source, dest)
}
diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go
index b074efa3a..c6c9ceb0c 100644
--- a/libpod/container_internal_linux.go
+++ b/libpod/container_internal_linux.go
@@ -18,14 +18,15 @@ import (
cnitypes "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/ns"
+ "github.com/containers/buildah/pkg/secrets"
crioAnnotations "github.com/containers/libpod/pkg/annotations"
"github.com/containers/libpod/pkg/apparmor"
"github.com/containers/libpod/pkg/criu"
"github.com/containers/libpod/pkg/lookup"
"github.com/containers/libpod/pkg/resolvconf"
"github.com/containers/libpod/pkg/rootless"
- "github.com/containers/libpod/pkg/secrets"
"github.com/containers/storage/pkg/idtools"
+ "github.com/cyphar/filepath-securejoin"
"github.com/opencontainers/runc/libcontainer/user"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
@@ -202,7 +203,8 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
}
// Check if the spec file mounts contain the label Relabel flags z or Z.
// If they do, relabel the source directory and then remove the option.
- for _, m := range g.Mounts() {
+ for i := range g.Config.Mounts {
+ m := &g.Config.Mounts[i]
var options []string
for _, o := range m.Options {
switch o {
@@ -218,6 +220,13 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
}
}
m.Options = options
+
+ // If we are using a user namespace, we will use an intermediate
+ // directory to bind mount volumes
+ if c.state.UserNSRoot != "" && strings.HasPrefix(m.Source, c.runtime.config.VolumePath) {
+ newSourceDir := filepath.Join(c.state.UserNSRoot, "volumes")
+ m.Source = strings.Replace(m.Source, c.runtime.config.VolumePath, newSourceDir, 1)
+ }
}
g.SetProcessSelinuxLabel(c.ProcessLabel())
@@ -366,6 +375,18 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
// For private volumes any root propagation value should work.
rootPropagation := ""
for _, m := range mounts {
+ // We need to remove all symlinks from tmpfs mounts.
+ // Runc and other runtimes may choke on them.
+ // Easy solution: use securejoin to do a scoped evaluation of
+ // the links, then trim off the mount prefix.
+ if m.Type == "tmpfs" {
+ finalPath, err := securejoin.SecureJoin(c.state.Mountpoint, m.Destination)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error resolving symlinks for mount destination %s", m.Destination)
+ }
+ trimmedPath := strings.TrimPrefix(finalPath, strings.TrimSuffix(c.state.Mountpoint, "/"))
+ m.Destination = trimmedPath
+ }
g.AddMount(m)
for _, opt := range m.Options {
switch opt {
@@ -472,10 +493,19 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr
return nil
}
-func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointOptions) (err error) {
-
+func (c *Container) checkpointRestoreSupported() (err error) {
if !criu.CheckForCriu() {
- return errors.Errorf("checkpointing a container requires at least CRIU %d", criu.MinCriuVersion)
+ return errors.Errorf("Checkpoint/Restore requires at least CRIU %d", criu.MinCriuVersion)
+ }
+ if !c.runtime.ociRuntime.featureCheckCheckpointing() {
+ return errors.Errorf("Configured runtime does not support checkpoint/restore")
+ }
+ return nil
+}
+
+func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointOptions) (err error) {
+ if err := c.checkpointRestoreSupported(); err != nil {
+ return err
}
if c.state.State != ContainerStateRunning {
@@ -532,8 +562,8 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
func (c *Container) restore(ctx context.Context, options ContainerCheckpointOptions) (err error) {
- if !criu.CheckForCriu() {
- return errors.Errorf("restoring a container requires at least CRIU %d", criu.MinCriuVersion)
+ if err := c.checkpointRestoreSupported(); err != nil {
+ return err
}
if (c.state.State != ContainerStateConfigured) && (c.state.State != ContainerStateExited) {
@@ -656,18 +686,21 @@ func (c *Container) makeBindMounts() error {
if !netDisabled {
// If /etc/resolv.conf and /etc/hosts exist, delete them so we
- // will recreate
- if path, ok := c.state.BindMounts["/etc/resolv.conf"]; ok {
- if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
- return errors.Wrapf(err, "error removing container %s resolv.conf", c.ID())
+ // will recreate. Only do this if we aren't sharing them with
+ // another container.
+ if c.config.NetNsCtr == "" {
+ if path, ok := c.state.BindMounts["/etc/resolv.conf"]; ok {
+ if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
+ return errors.Wrapf(err, "error removing container %s resolv.conf", c.ID())
+ }
+ delete(c.state.BindMounts, "/etc/resolv.conf")
}
- delete(c.state.BindMounts, "/etc/resolv.conf")
- }
- if path, ok := c.state.BindMounts["/etc/hosts"]; ok {
- if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
- return errors.Wrapf(err, "error removing container %s hosts", c.ID())
+ if path, ok := c.state.BindMounts["/etc/hosts"]; ok {
+ if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
+ return errors.Wrapf(err, "error removing container %s hosts", c.ID())
+ }
+ delete(c.state.BindMounts, "/etc/hosts")
}
- delete(c.state.BindMounts, "/etc/hosts")
}
if c.config.NetNsCtr != "" {
@@ -689,13 +722,29 @@ func (c *Container) makeBindMounts() error {
// If it doesn't, don't copy them
resolvPath, exists := bindMounts["/etc/resolv.conf"]
if exists {
-
c.state.BindMounts["/etc/resolv.conf"] = resolvPath
}
+
+ // check if dependency container has an /etc/hosts file
hostsPath, exists := bindMounts["/etc/hosts"]
- if exists {
- c.state.BindMounts["/etc/hosts"] = hostsPath
+ if !exists {
+ return errors.Errorf("error finding hosts file of dependency container %s for container %s", depCtr.ID(), c.ID())
+ }
+
+ depCtr.lock.Lock()
+ // generate a hosts file for the dependency container,
+ // based on either its old hosts file, or the default,
+ // and add the relevant information from the new container (hosts and IP)
+ hostsPath, err = depCtr.appendHosts(hostsPath, c)
+
+ if err != nil {
+ depCtr.lock.Unlock()
+ return errors.Wrapf(err, "error creating hosts file for container %s which depends on container %s", c.ID(), depCtr.ID())
}
+ depCtr.lock.Unlock()
+
+ // finally, save it in the new container
+ c.state.BindMounts["/etc/hosts"] = hostsPath
} else {
newResolv, err := c.generateResolvConf()
if err != nil {
@@ -703,7 +752,7 @@ func (c *Container) makeBindMounts() error {
}
c.state.BindMounts["/etc/resolv.conf"] = newResolv
- newHosts, err := c.generateHosts()
+ newHosts, err := c.generateHosts("/etc/hosts")
if err != nil {
return errors.Wrapf(err, "error creating hosts file for container %s", c.ID())
}
@@ -807,6 +856,10 @@ func (c *Container) generateResolvConf() (string, error) {
// Make a new resolv.conf
nameservers := resolvconf.GetNameservers(resolv.Content)
+ // slirp4netns has a built in DNS server.
+ if c.config.NetMode.IsSlirp4netns() {
+ nameservers = append(nameservers, "10.0.2.3")
+ }
if len(c.config.DNSServer) > 0 {
// We store DNS servers as net.IP, so need to convert to string
nameservers = []string{}
@@ -845,12 +898,28 @@ func (c *Container) generateResolvConf() (string, error) {
}
// generateHosts creates a containers hosts file
-func (c *Container) generateHosts() (string, error) {
- orig, err := ioutil.ReadFile("/etc/hosts")
+func (c *Container) generateHosts(path string) (string, error) {
+ orig, err := ioutil.ReadFile(path)
if err != nil {
- return "", errors.Wrapf(err, "unable to read /etc/hosts")
+ return "", errors.Wrapf(err, "unable to read %s", path)
}
hosts := string(orig)
+ hosts += c.getHosts()
+ return c.writeStringToRundir("hosts", hosts)
+}
+
+// appendHosts appends a container's config and state pertaining to hosts to a container's
+// local hosts file. netCtr is the container from which the netNS information is
+// taken.
+// path is the basis of the hosts file, into which netCtr's netNS information will be appended.
+func (c *Container) appendHosts(path string, netCtr *Container) (string, error) {
+ return c.appendStringToRundir("hosts", netCtr.getHosts())
+}
+
+// getHosts finds the pertinent information for a container's host file in its config and state
+// and returns a string in a format that can be written to the host file
+func (c *Container) getHosts() string {
+ var hosts string
if len(c.config.HostAdd) > 0 {
for _, host := range c.config.HostAdd {
// the host format has already been verified at this point
@@ -862,7 +931,7 @@ func (c *Container) generateHosts() (string, error) {
ipAddress := strings.Split(c.state.NetworkStatus[0].IPs[0].Address.String(), "/")[0]
hosts += fmt.Sprintf("%s\t%s\n", ipAddress, c.Hostname())
}
- return c.writeStringToRundir("hosts", hosts)
+ return hosts
}
// generatePasswd generates a container specific passwd file,
@@ -921,3 +990,20 @@ func (c *Container) generatePasswd() (string, error) {
}
return passwdFile, nil
}
+
+func (c *Container) copyOwnerAndPerms(source, dest string) error {
+ info, err := os.Stat(source)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return errors.Wrapf(err, "cannot stat `%s`", dest)
+ }
+ if err := os.Chmod(dest, info.Mode()); err != nil {
+ return errors.Wrapf(err, "cannot chmod `%s`", dest)
+ }
+ if err := os.Chown(dest, int(info.Sys().(*syscall.Stat_t).Uid), int(info.Sys().(*syscall.Stat_t).Gid)); err != nil {
+ return errors.Wrapf(err, "cannot chown `%s`", dest)
+ }
+ return nil
+}
diff --git a/libpod/container_internal_test.go b/libpod/container_internal_test.go
index f1e2b70a7..1654af929 100644
--- a/libpod/container_internal_test.go
+++ b/libpod/container_internal_test.go
@@ -17,6 +17,54 @@ import (
// hookPath is the path to an example hook executable.
var hookPath string
+func TestLocaleToLanguage(t *testing.T) {
+ for _, testCase := range []struct {
+ locale string
+ language string
+ }{
+ {
+ locale: "",
+ language: "und-u-va-posix",
+ },
+ {
+ locale: "C",
+ language: "und-u-va-posix",
+ },
+ {
+ locale: "POSIX",
+ language: "und-u-va-posix",
+ },
+ {
+ locale: "c",
+ language: "und-u-va-posix",
+ },
+ {
+ locale: "en",
+ language: "en",
+ },
+ {
+ locale: "en_US",
+ language: "en-US",
+ },
+ {
+ locale: "en.UTF-8",
+ language: "en",
+ },
+ {
+ locale: "en_US.UTF-8",
+ language: "en-US",
+ },
+ {
+ locale: "does-not-exist",
+ language: "does-not-exist",
+ },
+ } {
+ t.Run(testCase.locale, func(t *testing.T) {
+ assert.Equal(t, testCase.language, localeToLanguage(testCase.locale))
+ })
+ }
+}
+
func TestPostDeleteHooks(t *testing.T) {
ctx := context.Background()
dir, err := ioutil.TempDir("", "libpod_test_")
diff --git a/libpod/container_internal_unsupported.go b/libpod/container_internal_unsupported.go
index 4af0cd56c..f707b350c 100644
--- a/libpod/container_internal_unsupported.go
+++ b/libpod/container_internal_unsupported.go
@@ -35,3 +35,7 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
func (c *Container) restore(ctx context.Context, options ContainerCheckpointOptions) error {
return ErrNotImplemented
}
+
+func (c *Container) copyOwnerAndPerms(source, dest string) error {
+ return nil
+}
diff --git a/libpod/container_log.go b/libpod/container_log.go
new file mode 100644
index 000000000..e998ad316
--- /dev/null
+++ b/libpod/container_log.go
@@ -0,0 +1,213 @@
+package libpod
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/hpcloud/tail"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+const (
+ // logTimeFormat is the time format used in the log.
+ // It is a modified version of RFC3339Nano that guarantees trailing
+ // zeroes are not trimmed, taken from
+ // https://github.com/golang/go/issues/19635
+ logTimeFormat = "2006-01-02T15:04:05.000000000Z07:00"
+)
+
+// LogOptions is the options you can use for logs
+type LogOptions struct {
+ Details bool
+ Follow bool
+ Since time.Time
+ Tail uint64
+ Timestamps bool
+ Multi bool
+ WaitGroup *sync.WaitGroup
+}
+
+// LogLine describes the information for each line of a log
+type LogLine struct {
+ Device string
+ ParseLogType string
+ Time time.Time
+ Msg string
+ CID string
+}
+
+// Log is a runtime function that can read one or more container logs.
+func (r *Runtime) Log(containers []*Container, options *LogOptions, logChannel chan *LogLine) error {
+ for _, ctr := range containers {
+ if err := ctr.ReadLog(options, logChannel); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// ReadLog reads a containers log based on the input options and returns loglines over a channel
+func (c *Container) ReadLog(options *LogOptions, logChannel chan *LogLine) error {
+ t, tailLog, err := getLogFile(c.LogPath(), options)
+ if err != nil {
+ // If the log file does not exist, this is not fatal.
+ if os.IsNotExist(errors.Cause(err)) {
+ return nil
+ }
+ return errors.Wrapf(err, "unable to read log file %s for %s ", c.ID(), c.LogPath())
+ }
+ options.WaitGroup.Add(1)
+ if len(tailLog) > 0 {
+ for _, nll := range tailLog {
+ nll.CID = c.ID()
+ if nll.Since(options.Since) {
+ logChannel <- nll
+ }
+ }
+ }
+
+ go func() {
+ var partial string
+ for line := range t.Lines {
+ nll, err := newLogLine(line.Text)
+ if err != nil {
+ logrus.Error(err)
+ continue
+ }
+ if nll.Partial() {
+ partial = partial + nll.Msg
+ continue
+ } else if !nll.Partial() && len(partial) > 1 {
+ nll.Msg = partial
+ partial = ""
+ }
+ nll.CID = c.ID()
+ if nll.Since(options.Since) {
+ logChannel <- nll
+ }
+ }
+ options.WaitGroup.Done()
+ }()
+ return nil
+}
+
+// getLogFile returns an hp tail for a container given options
+func getLogFile(path string, options *LogOptions) (*tail.Tail, []*LogLine, error) {
+ var (
+ whence int
+ err error
+ logTail []*LogLine
+ )
+ // whence 0=origin, 2=end
+ if options.Tail > 0 {
+ whence = 2
+ logTail, err = getTailLog(path, int(options.Tail))
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+ seek := tail.SeekInfo{
+ Offset: 0,
+ Whence: whence,
+ }
+
+ t, err := tail.TailFile(path, tail.Config{MustExist: true, Poll: true, Follow: options.Follow, Location: &seek, Logger: tail.DiscardingLogger})
+ return t, logTail, err
+}
+
+func getTailLog(path string, tail int) ([]*LogLine, error) {
+ var (
+ tailLog []*LogLine
+ nlls []*LogLine
+ tailCounter int
+ partial string
+ )
+ content, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ splitContent := strings.Split(string(content), "\n")
+ // We read the content in reverse and add each nll until we have the same
+ // number of F type messages as the desired tail
+ for i := len(splitContent) - 1; i >= 0; i-- {
+ if len(splitContent[i]) == 0 {
+ continue
+ }
+ nll, err := newLogLine(splitContent[i])
+ if err != nil {
+ return nil, err
+ }
+ nlls = append(nlls, nll)
+ if !nll.Partial() {
+ tailCounter = tailCounter + 1
+ }
+ if tailCounter == tail {
+ break
+ }
+ }
+ // Now we iterate the results and assemble partial messages to become full messages
+ for _, nll := range nlls {
+ if nll.Partial() {
+ partial = partial + nll.Msg
+ } else {
+ nll.Msg = nll.Msg + partial
+ tailLog = append(tailLog, nll)
+ partial = ""
+ }
+ }
+ return tailLog, nil
+}
+
+// String converts a logline to a string for output given whether a detail
+// bool is specified.
+func (l *LogLine) String(options *LogOptions) string {
+ var out string
+ if options.Multi {
+ cid := l.CID
+ if len(cid) > 12 {
+ cid = cid[:12]
+ }
+ out = fmt.Sprintf("%s ", cid)
+ }
+ if options.Timestamps {
+ out = out + fmt.Sprintf("%s ", l.Time.Format(logTimeFormat))
+ }
+ return out + l.Msg
+}
+
+// Since returns a bool as to whether a log line occurred after a given time
+func (l *LogLine) Since(since time.Time) bool {
+ return l.Time.After(since)
+}
+
+// newLogLine creates a logLine struct from a container log string
+func newLogLine(line string) (*LogLine, error) {
+ splitLine := strings.Split(line, " ")
+ if len(splitLine) < 4 {
+ return nil, errors.Errorf("'%s' is not a valid container log line", line)
+ }
+ logTime, err := time.Parse(time.RFC3339Nano, splitLine[0])
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to convert time %s from container log", splitLine[0])
+ }
+ l := LogLine{
+ Time: logTime,
+ Device: splitLine[1],
+ ParseLogType: splitLine[2],
+ Msg: strings.Join(splitLine[3:], " "),
+ }
+ return &l, nil
+}
+
+// Partial returns a bool if the log line is a partial log type
+func (l *LogLine) Partial() bool {
+ if l.ParseLogType == "P" {
+ return true
+ }
+ return false
+}
diff --git a/libpod/events.go b/libpod/events.go
new file mode 100644
index 000000000..139600982
--- /dev/null
+++ b/libpod/events.go
@@ -0,0 +1,96 @@
+package libpod
+
+import (
+ "os"
+
+ "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)
+ }
+}
+
+// newContainerExitedEvent creates a new event for a container's death
+func (c *Container) newContainerExitedEvent(exitCode int32) {
+ e := events.NewEvent(events.Exited)
+ e.ID = c.ID()
+ e.Name = c.Name()
+ 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)
+ }
+}
+
+// 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: 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})
+}
diff --git a/libpod/events/events.go b/libpod/events/events.go
new file mode 100644
index 000000000..7db36653e
--- /dev/null
+++ b/libpod/events/events.go
@@ -0,0 +1,268 @@
+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"
+ // 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"
+ // 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 Exited.String():
+ return Exited, 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)
+}
diff --git a/libpod/healthcheck.go b/libpod/healthcheck.go
new file mode 100644
index 000000000..d8f56860b
--- /dev/null
+++ b/libpod/healthcheck.go
@@ -0,0 +1,320 @@
+package libpod
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/containers/libpod/pkg/inspect"
+ "github.com/coreos/go-systemd/dbus"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+// HealthCheckStatus represents the current state of a container
+type HealthCheckStatus int
+
+const (
+ // HealthCheckSuccess means the health worked
+ HealthCheckSuccess HealthCheckStatus = iota
+ // HealthCheckFailure means the health ran and failed
+ HealthCheckFailure HealthCheckStatus = iota
+ // HealthCheckContainerStopped means the health check cannot
+ // be run because the container is stopped
+ HealthCheckContainerStopped HealthCheckStatus = iota
+ // HealthCheckContainerNotFound means the container could
+ // not be found in local store
+ HealthCheckContainerNotFound HealthCheckStatus = iota
+ // HealthCheckNotDefined means the container has no health
+ // check defined in it
+ HealthCheckNotDefined HealthCheckStatus = iota
+ // HealthCheckInternalError means somes something failed obtaining or running
+ // a given health check
+ HealthCheckInternalError HealthCheckStatus = iota
+ // HealthCheckDefined means the healthcheck was found on the container
+ HealthCheckDefined HealthCheckStatus = iota
+
+ // MaxHealthCheckNumberLogs is the maximum number of attempts we keep
+ // in the healtcheck history file
+ MaxHealthCheckNumberLogs int = 5
+ // MaxHealthCheckLogLength in characters
+ MaxHealthCheckLogLength = 500
+
+ // HealthCheckHealthy describes a healthy container
+ HealthCheckHealthy string = "healthy"
+ // HealthCheckUnhealthy describes an unhealthy container
+ HealthCheckUnhealthy string = "unhealthy"
+ // HealthCheckStarting describes the time between when the container starts
+ // and the start-period (time allowed for the container to start and application
+ // to be running) expires.
+ HealthCheckStarting string = "starting"
+)
+
+// hcWriteCloser allows us to use bufio as a WriteCloser
+type hcWriteCloser struct {
+ *bufio.Writer
+}
+
+// Used to add a closer to bufio
+func (hcwc hcWriteCloser) Close() error {
+ return nil
+}
+
+// HealthCheck verifies the state and validity of the healthcheck configuration
+// on the container and then executes the healthcheck
+func (r *Runtime) HealthCheck(name string) (HealthCheckStatus, error) {
+ container, err := r.LookupContainer(name)
+ if err != nil {
+ return HealthCheckContainerNotFound, errors.Wrapf(err, "unable to lookup %s to perform a health check", name)
+ }
+ hcStatus, err := checkHealthCheckCanBeRun(container)
+ if err == nil {
+ return container.runHealthCheck()
+ }
+ return hcStatus, err
+}
+
+// runHealthCheck runs the health check as defined by the container
+func (c *Container) runHealthCheck() (HealthCheckStatus, error) {
+ var (
+ newCommand []string
+ returnCode int
+ capture bytes.Buffer
+ inStartPeriod bool
+ )
+ hcStatus, err := checkHealthCheckCanBeRun(c)
+ if err != nil {
+ return hcStatus, err
+ }
+ hcCommand := c.HealthCheckConfig().Test
+ if len(hcCommand) > 0 && hcCommand[0] == "CMD-SHELL" {
+ newCommand = []string{"sh", "-c", strings.Join(hcCommand[1:], " ")}
+ } else {
+ newCommand = hcCommand
+ }
+ captureBuffer := bufio.NewWriter(&capture)
+ hcw := hcWriteCloser{
+ captureBuffer,
+ }
+ streams := new(AttachStreams)
+ streams.OutputStream = hcw
+ streams.ErrorStream = hcw
+ streams.InputStream = os.Stdin
+ streams.AttachOutput = true
+ streams.AttachError = true
+ streams.AttachInput = true
+
+ logrus.Debugf("executing health check command %s for %s", strings.Join(newCommand, " "), c.ID())
+ timeStart := time.Now()
+ hcResult := HealthCheckSuccess
+ hcErr := c.Exec(false, false, []string{}, newCommand, "", "", streams, 0)
+ if hcErr != nil {
+ hcResult = HealthCheckFailure
+ returnCode = 1
+ }
+ timeEnd := time.Now()
+ if c.HealthCheckConfig().StartPeriod > 0 {
+ // there is a start-period we need to honor; we add startPeriod to container start time
+ startPeriodTime := c.state.StartedTime.Add(c.HealthCheckConfig().StartPeriod)
+ if timeStart.Before(startPeriodTime) {
+ // we are still in the start period, flip the inStartPeriod bool
+ inStartPeriod = true
+ logrus.Debugf("healthcheck for %s being run in start-period", c.ID())
+ }
+ }
+
+ eventLog := capture.String()
+ if len(eventLog) > MaxHealthCheckLogLength {
+ eventLog = eventLog[:MaxHealthCheckLogLength]
+ }
+
+ if timeEnd.Sub(timeStart) > c.HealthCheckConfig().Timeout {
+ returnCode = -1
+ hcResult = HealthCheckFailure
+ hcErr = errors.Errorf("healthcheck command exceeded timeout of %s", c.HealthCheckConfig().Timeout.String())
+ }
+ hcl := newHealthCheckLog(timeStart, timeEnd, returnCode, eventLog)
+ if err := c.updateHealthCheckLog(hcl, inStartPeriod); err != nil {
+ return hcResult, errors.Wrapf(err, "unable to update health check log %s for %s", c.healthCheckLogPath(), c.ID())
+ }
+ return hcResult, hcErr
+}
+
+func checkHealthCheckCanBeRun(c *Container) (HealthCheckStatus, error) {
+ cstate, err := c.State()
+ if err != nil {
+ return HealthCheckInternalError, err
+ }
+ if cstate != ContainerStateRunning {
+ return HealthCheckContainerStopped, errors.Errorf("container %s is not running", c.ID())
+ }
+ if !c.HasHealthCheck() {
+ return HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID())
+ }
+ return HealthCheckDefined, nil
+}
+
+func newHealthCheckLog(start, end time.Time, exitCode int, log string) inspect.HealthCheckLog {
+ return inspect.HealthCheckLog{
+ Start: start.Format(time.RFC3339Nano),
+ End: end.Format(time.RFC3339Nano),
+ ExitCode: exitCode,
+ Output: log,
+ }
+}
+
+// updatedHealthCheckStatus updates the health status of the container
+// in the healthcheck log
+func (c *Container) updateHealthStatus(status string) error {
+ healthCheck, err := c.GetHealthCheckLog()
+ if err != nil {
+ return err
+ }
+ healthCheck.Status = status
+ newResults, err := json.Marshal(healthCheck)
+ if err != nil {
+ return errors.Wrapf(err, "unable to marshall healthchecks for writing status")
+ }
+ return ioutil.WriteFile(c.healthCheckLogPath(), newResults, 0700)
+}
+
+// UpdateHealthCheckLog parses the health check results and writes the log
+func (c *Container) updateHealthCheckLog(hcl inspect.HealthCheckLog, inStartPeriod bool) error {
+ healthCheck, err := c.GetHealthCheckLog()
+ if err != nil {
+ return err
+ }
+ if hcl.ExitCode == 0 {
+ // set status to healthy, reset failing state to 0
+ healthCheck.Status = HealthCheckHealthy
+ healthCheck.FailingStreak = 0
+ } else {
+ if len(healthCheck.Status) < 1 {
+ healthCheck.Status = HealthCheckHealthy
+ }
+ if !inStartPeriod {
+ // increment failing streak
+ healthCheck.FailingStreak = healthCheck.FailingStreak + 1
+ // if failing streak > retries, then status to unhealthy
+ if int(healthCheck.FailingStreak) >= c.HealthCheckConfig().Retries {
+ healthCheck.Status = HealthCheckUnhealthy
+ }
+ }
+ }
+ healthCheck.Log = append(healthCheck.Log, hcl)
+ if len(healthCheck.Log) > MaxHealthCheckNumberLogs {
+ healthCheck.Log = healthCheck.Log[1:]
+ }
+ newResults, err := json.Marshal(healthCheck)
+ if err != nil {
+ return errors.Wrapf(err, "unable to marshall healthchecks for writing")
+ }
+ return ioutil.WriteFile(c.healthCheckLogPath(), newResults, 0700)
+}
+
+// HealthCheckLogPath returns the path for where the health check log is
+func (c *Container) healthCheckLogPath() string {
+ return filepath.Join(filepath.Dir(c.LogPath()), "healthcheck.log")
+}
+
+// GetHealthCheckLog returns HealthCheck results by reading the container's
+// health check log file. If the health check log file does not exist, then
+// an empty healthcheck struct is returned
+func (c *Container) GetHealthCheckLog() (inspect.HealthCheckResults, error) {
+ var healthCheck inspect.HealthCheckResults
+ if _, err := os.Stat(c.healthCheckLogPath()); os.IsNotExist(err) {
+ return healthCheck, nil
+ }
+ b, err := ioutil.ReadFile(c.healthCheckLogPath())
+ if err != nil {
+ return healthCheck, errors.Wrapf(err, "failed to read health check log file %s", c.healthCheckLogPath())
+ }
+ if err := json.Unmarshal(b, &healthCheck); err != nil {
+ return healthCheck, errors.Wrapf(err, "failed to unmarshal existing healthcheck results in %s", c.healthCheckLogPath())
+ }
+ return healthCheck, nil
+}
+
+// createTimer systemd timers for healthchecks of a container
+func (c *Container) createTimer() error {
+ if c.disableHealthCheckSystemd() {
+ return nil
+ }
+ podman, err := os.Executable()
+ if err != nil {
+ return errors.Wrapf(err, "failed to get path for podman for a health check timer")
+ }
+
+ var cmd = []string{"--unit", fmt.Sprintf("%s", c.ID()), fmt.Sprintf("--on-unit-inactive=%s", c.HealthCheckConfig().Interval.String()), "--timer-property=AccuracySec=1s", podman, "healthcheck", "run", c.ID()}
+
+ conn, err := dbus.NewSystemdConnection()
+ if err != nil {
+ return errors.Wrapf(err, "unable to get systemd connection to add healthchecks")
+ }
+ conn.Close()
+ logrus.Debugf("creating systemd-transient files: %s %s", "systemd-run", cmd)
+ systemdRun := exec.Command("systemd-run", cmd...)
+ _, err = systemdRun.CombinedOutput()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// startTimer starts a systemd timer for the healthchecks
+func (c *Container) startTimer() error {
+ if c.disableHealthCheckSystemd() {
+ return nil
+ }
+ conn, err := dbus.NewSystemdConnection()
+ if err != nil {
+ return errors.Wrapf(err, "unable to get systemd connection to start healthchecks")
+ }
+ defer conn.Close()
+ _, err = conn.StartUnit(fmt.Sprintf("%s.service", c.ID()), "fail", nil)
+ return err
+}
+
+// removeTimer removes the systemd timer and unit files
+// for the container
+func (c *Container) removeTimer() error {
+ if c.disableHealthCheckSystemd() {
+ return nil
+ }
+ conn, err := dbus.NewSystemdConnection()
+ if err != nil {
+ return errors.Wrapf(err, "unable to get systemd connection to remove healthchecks")
+ }
+ defer conn.Close()
+ serviceFile := fmt.Sprintf("%s.timer", c.ID())
+ _, err = conn.StopUnit(serviceFile, "fail", nil)
+ return err
+}
+
+// HealthCheckStatus returns the current state of a container with a healthcheck
+func (c *Container) HealthCheckStatus() (string, error) {
+ if !c.HasHealthCheck() {
+ return "", errors.Errorf("container %s has no defined healthcheck", c.ID())
+ }
+ results, err := c.GetHealthCheckLog()
+ if err != nil {
+ return "", errors.Wrapf(err, "unable to get healthcheck log for %s", c.ID())
+ }
+ return results.Status, nil
+}
+
+func (c *Container) disableHealthCheckSystemd() bool {
+ if os.Getenv("DISABLE_HC_SYSTEMD") == "true" {
+ return true
+ }
+ if c.config.HealthCheckConfig.Interval == 0 {
+ return true
+ }
+ return false
+}
diff --git a/libpod/image/image.go b/libpod/image/image.go
index b20419d7b..f79bc3dc2 100644
--- a/libpod/image/image.go
+++ b/libpod/image/image.go
@@ -24,12 +24,13 @@ import (
"github.com/containers/image/types"
"github.com/containers/libpod/libpod/common"
"github.com/containers/libpod/libpod/driver"
+ "github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/inspect"
"github.com/containers/libpod/pkg/registries"
"github.com/containers/libpod/pkg/util"
"github.com/containers/storage"
"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"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opentracing/opentracing-go"
@@ -64,6 +65,7 @@ type Image struct {
type Runtime struct {
store storage.Store
SignaturePolicyPath string
+ EventsLogFilePath string
}
// 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
newImages = append(newImages, &newImage)
}
-
+ ir.newImageEvent(events.LoadFromArchive, "")
return newImages, nil
}
@@ -371,6 +373,7 @@ func (i *Image) Remove(force bool) error {
}
parent = nextParent
}
+ defer i.newImageEvent(events.Remove)
return nil
}
@@ -494,6 +497,7 @@ func (i *Image) TagImage(tag string) error {
return err
}
i.reloadImage()
+ defer i.newImageEvent(events.Tag)
return nil
}
@@ -514,6 +518,7 @@ func (i *Image) UntagImage(tag string) error {
return err
}
i.reloadImage()
+ defer i.newImageEvent(events.Untag)
return nil
}
@@ -562,6 +567,7 @@ func (i *Image) PushImageToReference(ctx context.Context, dest types.ImageRefere
if err != nil {
return errors.Wrapf(err, "Error copying image to the remote destination")
}
+ defer i.newImageEvent(events.Push)
return nil
}
@@ -711,7 +717,6 @@ func (i *Image) History(ctx context.Context) ([]*History, error) {
Comment: oci.History[i].Comment,
})
}
-
return allHistory, nil
}
@@ -927,7 +932,11 @@ func (ir *Runtime) Import(ctx context.Context, path, reference string, writer io
if err != nil {
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
@@ -1143,11 +1152,127 @@ func (i *Image) Save(ctx context.Context, source, format, output string, moreTag
}
}
if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", writer, compress, SigningOptions{}, &DockerRegistryOptions{}, additionaltags); err != nil {
- if err2 := os.Remove(output); err2 != nil {
- logrus.Errorf("error deleting %q: %v", output, err)
- }
return errors.Wrapf(err, "unable to save %q", source)
}
-
+ defer i.newImageEvent(events.Save)
return nil
}
+
+// GetConfigBlob returns a schema2image. If the image is not a schema2, then
+// it will return an error
+func (i *Image) GetConfigBlob(ctx context.Context) (*manifest.Schema2Image, error) {
+ imageRef, err := i.toImageRef(ctx)
+ if err != nil {
+ return nil, err
+ }
+ b, err := imageRef.ConfigBlob(ctx)
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to get config blob for %s", i.ID())
+ }
+ blob := manifest.Schema2Image{}
+ if err := json.Unmarshal(b, &blob); err != nil {
+ return nil, errors.Wrapf(err, "unable to parse image blob for %s", i.ID())
+ }
+ return &blob, nil
+
+}
+
+// GetHealthCheck returns a HealthConfig for an image. This function only works with
+// schema2 images.
+func (i *Image) GetHealthCheck(ctx context.Context) (*manifest.Schema2HealthConfig, error) {
+ configBlob, err := i.GetConfigBlob(ctx)
+ if err != nil {
+ return nil, err
+ }
+ 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)
+ }
+}
+
+// LayerInfo keeps information of single layer
+type LayerInfo struct {
+ // Layer ID
+ ID string
+ // Parent ID of current layer.
+ ParentID string
+ // ChildID of current layer.
+ // there can be multiple children in case of fork
+ ChildID []string
+ // RepoTag will have image repo names, if layer is top layer of image
+ RepoTags []string
+ // Size stores Uncompressed size of layer.
+ Size int64
+}
+
+// GetLayersMapWithImageInfo returns map of image-layers, with associated information like RepoTags, parent and list of child layers.
+func GetLayersMapWithImageInfo(imageruntime *Runtime) (map[string]*LayerInfo, error) {
+
+ // Memory allocated to store map of layers with key LayerID.
+ // Map will build dependency chain with ParentID and ChildID(s)
+ layerInfoMap := make(map[string]*LayerInfo)
+
+ // scan all layers & fill size and parent id for each layer in layerInfoMap
+ layers, err := imageruntime.store.Layers()
+ if err != nil {
+ return nil, err
+ }
+ for _, layer := range layers {
+ _, ok := layerInfoMap[layer.ID]
+ if !ok {
+ layerInfoMap[layer.ID] = &LayerInfo{
+ ID: layer.ID,
+ Size: layer.UncompressedSize,
+ ParentID: layer.Parent,
+ }
+ } else {
+ return nil, fmt.Errorf("detected multiple layers with the same ID %q", layer.ID)
+ }
+ }
+
+ // scan all layers & add all childs for each layers to layerInfo
+ for _, layer := range layers {
+ _, ok := layerInfoMap[layer.ID]
+ if ok {
+ if layer.Parent != "" {
+ layerInfoMap[layer.Parent].ChildID = append(layerInfoMap[layer.Parent].ChildID, layer.ID)
+ }
+ } else {
+ return nil, fmt.Errorf("lookup error: layer-id %s, not found", layer.ID)
+ }
+ }
+
+ // Add the Repo Tags to Top layer of each image.
+ imgs, err := imageruntime.store.Images()
+ if err != nil {
+ return nil, err
+ }
+ for _, img := range imgs {
+ e, ok := layerInfoMap[img.TopLayer]
+ if !ok {
+ return nil, fmt.Errorf("top-layer for image %s not found local store", img.ID)
+ }
+ e.RepoTags = append(e.RepoTags, img.Names...)
+ }
+ return layerInfoMap, nil
+}
diff --git a/libpod/image/prune.go b/libpod/image/prune.go
index 8602c222c..5bd3c2c99 100644
--- a/libpod/image/prune.go
+++ b/libpod/image/prune.go
@@ -1,6 +1,9 @@
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
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 {
return nil, errors.Wrap(err, "failed to prune image")
}
+ defer p.newImageEvent(events.Prune)
prunedCids = append(prunedCids, p.ID())
}
return prunedCids, nil
diff --git a/libpod/image/pull.go b/libpod/image/pull.go
index 6b9f7fc67..a3b716e65 100644
--- a/libpod/image/pull.go
+++ b/libpod/image/pull.go
@@ -17,6 +17,7 @@ import (
"github.com/containers/image/transports"
"github.com/containers/image/transports/alltransports"
"github.com/containers/image/types"
+ "github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/registries"
multierror "github.com/hashicorp/go-multierror"
opentracing "github.com/opentracing/opentracing-go"
@@ -267,12 +268,13 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
_, err = cp.Image(ctx, policyContext, imageInfo.dstRef, imageInfo.srcRef, copyOptions)
if err != nil {
pullErrors = multierror.Append(pullErrors, err)
- logrus.Debugf("Error pulling image ref %s: %v", imageInfo.srcRef.StringWithinTransport(), err)
+ logrus.Errorf("Error pulling image ref %s: %v", imageInfo.srcRef.StringWithinTransport(), err)
if writer != nil {
io.WriteString(writer, "Failed\n")
}
} else {
if !goal.pullAllPairs {
+ ir.newImageEvent(events.Pull, "")
return []string{imageInfo.image}, nil
}
images = append(images, imageInfo.image)
@@ -293,6 +295,9 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
}
return nil, pullErrors
}
+ if len(images) > 0 {
+ defer ir.newImageEvent(events.Pull, images[0])
+ }
return images, nil
}
diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go
index f9caf26d1..2450bd6b1 100644
--- a/libpod/networking_linux.go
+++ b/libpod/networking_linux.go
@@ -134,15 +134,29 @@ type slirp4netnsCmd struct {
Args slirp4netnsCmdArg `json:"arguments"`
}
+func checkSlirpFlags(path string) (bool, bool, error) {
+ cmd := exec.Command(path, "--help")
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return false, false, err
+ }
+ return strings.Contains(string(out), "--disable-host-loopback"), strings.Contains(string(out), "--mtu"), nil
+}
+
// Configure the network namespace for a rootless container
func (r *Runtime) setupRootlessNetNS(ctr *Container) (err error) {
defer ctr.rootlessSlirpSyncR.Close()
defer ctr.rootlessSlirpSyncW.Close()
- path, err := exec.LookPath("slirp4netns")
- if err != nil {
- logrus.Errorf("could not find slirp4netns, the network namespace won't be configured: %v", err)
- return nil
+ path := r.config.NetworkCmdPath
+
+ if path == "" {
+ var err error
+ path, err = exec.LookPath("slirp4netns")
+ if err != nil {
+ logrus.Errorf("could not find slirp4netns, the network namespace won't be configured: %v", err)
+ return nil
+ }
}
syncR, syncW, err := os.Pipe()
@@ -154,13 +168,24 @@ func (r *Runtime) setupRootlessNetNS(ctr *Container) (err error) {
havePortMapping := len(ctr.Config().PortMappings) > 0
apiSocket := filepath.Join(r.ociRuntime.tmpDir, fmt.Sprintf("%s.net", ctr.config.ID))
- var cmd *exec.Cmd
+
+ cmdArgs := []string{}
if havePortMapping {
- // if we need ports to be mapped from the host, create a API socket to use for communicating with slirp4netns.
- cmd = exec.Command(path, "-c", "-e", "3", "-r", "4", "--api-socket", apiSocket, fmt.Sprintf("%d", ctr.state.PID), "tap0")
- } else {
- cmd = exec.Command(path, "-c", "-e", "3", "-r", "4", fmt.Sprintf("%d", ctr.state.PID), "tap0")
+ cmdArgs = append(cmdArgs, "--api-socket", apiSocket, fmt.Sprintf("%d", ctr.state.PID))
+ }
+ dhp, mtu, err := checkSlirpFlags(path)
+ if err != nil {
+ return errors.Wrapf(err, "error checking slirp4netns binary %s", path)
+ }
+ if dhp {
+ cmdArgs = append(cmdArgs, "--disable-host-loopback")
}
+ if mtu {
+ cmdArgs = append(cmdArgs, "--mtu", "65520")
+ }
+ cmdArgs = append(cmdArgs, "-c", "-e", "3", "-r", "4", fmt.Sprintf("%d", ctr.state.PID), "tap0")
+
+ cmd := exec.Command(path, cmdArgs...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
@@ -190,9 +215,12 @@ func (r *Runtime) setupRootlessNetNS(ctr *Container) (err error) {
if pid != cmd.Process.Pid {
continue
}
- if status.Exited() || status.Signaled() {
+ if status.Exited() {
return errors.New("slirp4netns failed")
}
+ if status.Signaled() {
+ return errors.New("slirp4netns killed by signal")
+ }
continue
}
return errors.Wrapf(err, "failed to read from slirp4netns sync pipe")
diff --git a/libpod/oci.go b/libpod/oci.go
index 4bf76f619..69cff6d3c 100644
--- a/libpod/oci.go
+++ b/libpod/oci.go
@@ -183,6 +183,7 @@ func waitPidsStop(pids []int, timeout time.Duration) error {
func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) {
var files []*os.File
+ notifySCTP := false
for _, i := range ports {
switch i.Protocol {
case "udp":
@@ -218,6 +219,12 @@ func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) {
}
files = append(files, f)
break
+ case "sctp":
+ if !notifySCTP {
+ notifySCTP = true
+ logrus.Warnf("port reservation for SCTP is not supported")
+ }
+ break
default:
return nil, fmt.Errorf("unknown protocol %s", i.Protocol)
@@ -477,7 +484,7 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container, useRunc bool) error {
// If not using runc, we don't need to do most of this.
if !useRunc {
// If the container's not running, nothing to do.
- if ctr.state.State != ContainerStateRunning {
+ if ctr.state.State != ContainerStateRunning && ctr.state.State != ContainerStatePaused {
return nil
}
@@ -733,7 +740,7 @@ func (r *OCIRuntime) unpauseContainer(ctr *Container) error {
// TODO: Add --detach support
// TODO: Convert to use conmon
// TODO: add --pid-file and use that to generate exec session tracking
-func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty bool, cwd, user, sessionID string, streams *AttachStreams) (*exec.Cmd, error) {
+func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty bool, cwd, user, sessionID string, streams *AttachStreams, preserveFDs int) (*exec.Cmd, error) {
if len(cmd) == 0 {
return nil, errors.Wrapf(ErrInvalidArg, "must provide a command to execute")
}
@@ -770,6 +777,9 @@ func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty
args = append(args, "--user", user)
}
+ if preserveFDs > 0 {
+ args = append(args, fmt.Sprintf("--preserve-fds=%d", preserveFDs))
+ }
if c.config.Spec.Process.NoNewPrivileges {
args = append(args, "--no-new-privs")
}
@@ -802,10 +812,24 @@ func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty
execCmd.Env = append(execCmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir))
+ if preserveFDs > 0 {
+ for fd := 3; fd < 3+preserveFDs; fd++ {
+ execCmd.ExtraFiles = append(execCmd.ExtraFiles, os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd)))
+ }
+ }
+
if err := execCmd.Start(); err != nil {
return nil, errors.Wrapf(err, "cannot start container %s", c.ID())
}
+ if preserveFDs > 0 {
+ for fd := 3; fd < 3+preserveFDs; fd++ {
+ // These fds were passed down to the runtime. Close them
+ // and not interfere
+ os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd)).Close()
+ }
+ }
+
return execCmd, nil
}
@@ -898,3 +922,16 @@ func (r *OCIRuntime) checkpointContainer(ctr *Container, options ContainerCheckp
args = append(args, ctr.ID())
return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, args...)
}
+
+func (r *OCIRuntime) featureCheckCheckpointing() bool {
+ // Check if the runtime implements checkpointing. Currently only
+ // runc's checkpoint/restore implementation is supported.
+ cmd := exec.Command(r.path, "checkpoint", "-h")
+ if err := cmd.Start(); err != nil {
+ return false
+ }
+ if err := cmd.Wait(); err == nil {
+ return true
+ }
+ return false
+}
diff --git a/libpod/oci_linux.go b/libpod/oci_linux.go
index 2737a641e..f85c5ee62 100644
--- a/libpod/oci_linux.go
+++ b/libpod/oci_linux.go
@@ -106,6 +106,23 @@ func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restor
if err != nil {
return
}
+
+ if ctr.state.UserNSRoot != "" {
+ _, err := os.Stat(ctr.runtime.config.VolumePath)
+ if err != nil && !os.IsNotExist(err) {
+ return
+ }
+ if err == nil {
+ volumesTarget := filepath.Join(ctr.state.UserNSRoot, "volumes")
+ if err := idtools.MkdirAs(volumesTarget, 0700, ctr.RootUID(), ctr.RootGID()); err != nil {
+ return
+ }
+ if err = unix.Mount(ctr.runtime.config.VolumePath, volumesTarget, "none", unix.MS_BIND, ""); err != nil {
+ return
+ }
+ }
+ }
+
err = r.createOCIContainer(ctr, cgroupParent, restoreOptions)
}()
wg.Wait()
diff --git a/libpod/options.go b/libpod/options.go
index 1e8592a25..1bf3ff9e6 100644
--- a/libpod/options.go
+++ b/libpod/options.go
@@ -7,6 +7,7 @@ import (
"regexp"
"syscall"
+ "github.com/containers/image/manifest"
"github.com/containers/libpod/pkg/namespaces"
"github.com/containers/storage"
"github.com/containers/storage/pkg/idtools"
@@ -192,6 +193,20 @@ func WithConmonEnv(environment []string) RuntimeOption {
}
}
+// WithNetworkCmdPath specifies the path to the slirp4netns binary which manages the
+// runtime.
+func WithNetworkCmdPath(path string) RuntimeOption {
+ return func(rt *Runtime) error {
+ if rt.valid {
+ return ErrRuntimeFinalized
+ }
+
+ rt.config.NetworkCmdPath = path
+
+ return nil
+ }
+}
+
// WithCgroupManager specifies the manager implementation name which is used to
// handle cgroups for containers.
// Current valid values are "cgroupfs" and "systemd".
@@ -271,7 +286,6 @@ func WithTmpDir(dir string) RuntimeOption {
if rt.valid {
return ErrRuntimeFinalized
}
-
rt.config.TmpDir = dir
rt.configuredFrom.libpodTmpDirSet = true
@@ -1469,3 +1483,14 @@ func WithInfraContainerPorts(bindings []ocicni.PortMapping) PodCreateOption {
return nil
}
}
+
+// WithHealthCheck adds the healthcheck to the container config
+func WithHealthCheck(healthCheck *manifest.Schema2HealthConfig) CtrCreateOption {
+ return func(ctr *Container) error {
+ if ctr.valid {
+ return ErrCtrFinalized
+ }
+ ctr.config.HealthCheckConfig = healthCheck
+ return nil
+ }
+}
diff --git a/libpod/pod_api.go b/libpod/pod_api.go
index cbac2420f..b9a11000e 100644
--- a/libpod/pod_api.go
+++ b/libpod/pod_api.go
@@ -3,6 +3,7 @@ package libpod
import (
"context"
+ "github.com/containers/libpod/libpod/events"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/ulule/deepcopier"
@@ -58,7 +59,7 @@ func (p *Pod) Start(ctx context.Context) (map[string]error, error) {
if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error starting some containers")
}
-
+ defer p.newPodEvent(events.Start)
return nil, nil
}
@@ -138,7 +139,7 @@ func (p *Pod) StopWithTimeout(ctx context.Context, cleanup bool, timeout int) (m
if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error stopping some containers")
}
-
+ defer p.newPodEvent(events.Stop)
return nil, nil
}
@@ -197,7 +198,7 @@ func (p *Pod) Pause() (map[string]error, error) {
if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error pausing some containers")
}
-
+ defer p.newPodEvent(events.Pause)
return nil, nil
}
@@ -257,6 +258,7 @@ func (p *Pod) Unpause() (map[string]error, error) {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error unpausing some containers")
}
+ defer p.newPodEvent(events.Unpause)
return nil, nil
}
@@ -309,7 +311,8 @@ func (p *Pod) Restart(ctx context.Context) (map[string]error, error) {
if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error stopping some containers")
}
-
+ p.newPodEvent(events.Stop)
+ p.newPodEvent(events.Start)
return nil, nil
}
@@ -367,7 +370,7 @@ func (p *Pod) Kill(signal uint) (map[string]error, error) {
if len(ctrErrors) > 0 {
return ctrErrors, errors.Wrapf(ErrCtrExists, "error killing some containers")
}
-
+ defer p.newPodEvent(events.Kill)
return nil, nil
}
diff --git a/libpod/runtime.go b/libpod/runtime.go
index f53cdd8b8..b3b75d791 100644
--- a/libpod/runtime.go
+++ b/libpod/runtime.go
@@ -217,10 +217,15 @@ type RuntimeConfig struct {
EnablePortReservation bool `toml:"enable_port_reservation"`
// EnableLabeling indicates wether libpod will support container labeling
EnableLabeling bool `toml:"label"`
+ // NetworkCmdPath is the path to the slirp4netns binary
+ NetworkCmdPath string `toml:"network_cmd_path"`
// NumLocks is the number of locks to make available for containers and
// pods.
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
@@ -236,6 +241,12 @@ type runtimeConfiguredFrom struct {
libpodStaticDirSet bool
libpodTmpDirSet bool
volPathSet bool
+ conmonPath bool
+ conmonEnvVars bool
+ ociRuntimes bool
+ runtimePath bool
+ cniPluginDir bool
+ noPivotRoot bool
}
var (
@@ -319,6 +330,22 @@ func SetXdgRuntimeDir(val string) error {
// NewRuntime creates a new container runtime
// Options can be passed to override the default configuration for the runtime
func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
+ return newRuntimeFromConfig("", options...)
+}
+
+// NewRuntimeFromConfig creates a new container runtime using the given
+// configuration file for its default configuration. Passed RuntimeOption
+// functions can be used to mutate this configuration further.
+// An error will be returned if the configuration file at the given path does
+// not exist or cannot be loaded
+func NewRuntimeFromConfig(userConfigPath string, options ...RuntimeOption) (runtime *Runtime, err error) {
+ if userConfigPath == "" {
+ return nil, errors.New("invalid configuration file specified")
+ }
+ return newRuntimeFromConfig(userConfigPath, options...)
+}
+
+func newRuntimeFromConfig(userConfigPath string, options ...RuntimeOption) (runtime *Runtime, err error) {
runtime = new(Runtime)
runtime.config = new(RuntimeConfig)
runtime.configuredFrom = new(runtimeConfiguredFrom)
@@ -333,7 +360,7 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
storageConf, err := util.GetDefaultStoreOptions()
if err != nil {
- return nil, errors.Wrapf(err, "error retrieving rootless storage config")
+ return nil, errors.Wrapf(err, "error retrieving storage config")
}
runtime.config.StorageConfig = storageConf
runtime.config.StaticDir = filepath.Join(storageConf.GraphRoot, "libpod")
@@ -353,11 +380,6 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
rootlessConfigPath = filepath.Join(home, ".config/containers/libpod.conf")
- configPath = rootlessConfigPath
- if _, err := os.Stat(configPath); err != nil {
- foundConfig = false
- }
-
runtimeDir, err := util.GetRootlessRuntimeDir()
if err != nil {
return nil, err
@@ -365,11 +387,24 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
// containers/image uses XDG_RUNTIME_DIR to locate the auth file.
// So make sure the env variable is set.
- err = SetXdgRuntimeDir(runtimeDir)
- if err != nil {
+ if err := SetXdgRuntimeDir(runtimeDir); err != nil {
return nil, errors.Wrapf(err, "cannot set XDG_RUNTIME_DIR")
}
+ }
+
+ if userConfigPath != "" {
+ configPath = userConfigPath
+ if _, err := os.Stat(configPath); err != nil {
+ // If the user specified a config file, we must fail immediately
+ // when it doesn't exist
+ return nil, errors.Wrapf(err, "cannot stat %s", configPath)
+ }
+ } else if rootless.IsRootless() {
+ configPath = rootlessConfigPath
+ if _, err := os.Stat(configPath); err != nil {
+ foundConfig = false
+ }
} else if _, err := os.Stat(OverrideConfigPath); err == nil {
// Use the override configuration path
configPath = OverrideConfigPath
@@ -405,6 +440,24 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
if tmpConfig.VolumePath != "" {
runtime.configuredFrom.volPathSet = true
}
+ if tmpConfig.ConmonPath != nil {
+ runtime.configuredFrom.conmonPath = true
+ }
+ if tmpConfig.ConmonEnvVars != nil {
+ runtime.configuredFrom.conmonEnvVars = true
+ }
+ if tmpConfig.OCIRuntimes != nil {
+ runtime.configuredFrom.ociRuntimes = true
+ }
+ if tmpConfig.RuntimePath != nil {
+ runtime.configuredFrom.runtimePath = true
+ }
+ if tmpConfig.CNIPluginDir != nil {
+ runtime.configuredFrom.cniPluginDir = true
+ }
+ if tmpConfig.NoPivotRoot {
+ runtime.configuredFrom.noPivotRoot = true
+ }
if _, err := toml.Decode(string(contents), runtime.config); err != nil {
return nil, errors.Wrapf(err, "error decoding configuration file %s", configPath)
@@ -424,12 +477,24 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
}
// Cherry pick the settings we want from the global configuration
- runtime.config.ConmonPath = tmpConfig.ConmonPath
- runtime.config.ConmonEnvVars = tmpConfig.ConmonEnvVars
- runtime.config.OCIRuntimes = tmpConfig.OCIRuntimes
- runtime.config.RuntimePath = tmpConfig.RuntimePath
- runtime.config.CNIPluginDir = tmpConfig.CNIPluginDir
- runtime.config.NoPivotRoot = tmpConfig.NoPivotRoot
+ if !runtime.configuredFrom.conmonPath {
+ runtime.config.ConmonPath = tmpConfig.ConmonPath
+ }
+ if !runtime.configuredFrom.conmonEnvVars {
+ runtime.config.ConmonEnvVars = tmpConfig.ConmonEnvVars
+ }
+ if !runtime.configuredFrom.ociRuntimes {
+ runtime.config.OCIRuntimes = tmpConfig.OCIRuntimes
+ }
+ if !runtime.configuredFrom.runtimePath {
+ runtime.config.RuntimePath = tmpConfig.RuntimePath
+ }
+ if !runtime.configuredFrom.cniPluginDir {
+ runtime.config.CNIPluginDir = tmpConfig.CNIPluginDir
+ }
+ if !runtime.configuredFrom.noPivotRoot {
+ runtime.config.NoPivotRoot = tmpConfig.NoPivotRoot
+ }
break
}
}
@@ -440,77 +505,39 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
return nil, errors.Wrapf(err, "error configuring runtime")
}
}
- if err := makeRuntime(runtime); err != nil {
- return nil, err
- }
-
- if !foundConfig && rootlessConfigPath != "" {
- os.MkdirAll(filepath.Dir(rootlessConfigPath), 0755)
- file, err := os.OpenFile(rootlessConfigPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
- if err != nil && !os.IsExist(err) {
- return nil, errors.Wrapf(err, "cannot open file %s", rootlessConfigPath)
- }
- if err == nil {
- defer file.Close()
- enc := toml.NewEncoder(file)
- if err := enc.Encode(runtime.config); err != nil {
- os.Remove(rootlessConfigPath)
+ if rootlessConfigPath != "" {
+ // storage.conf
+ storageConfFile := util.StorageConfigFile()
+ if _, err := os.Stat(storageConfFile); os.IsNotExist(err) {
+ if err := util.WriteStorageConfigFile(&runtime.config.StorageConfig, storageConfFile); err != nil {
+ return nil, errors.Wrapf(err, "cannot write config file %s", storageConfFile)
}
}
- }
-
- return runtime, nil
-}
-// NewRuntimeFromConfig creates a new container runtime using the given
-// configuration file for its default configuration. Passed RuntimeOption
-// functions can be used to mutate this configuration further.
-// An error will be returned if the configuration file at the given path does
-// not exist or cannot be loaded
-func NewRuntimeFromConfig(configPath string, options ...RuntimeOption) (runtime *Runtime, err error) {
- runtime = new(Runtime)
- runtime.config = new(RuntimeConfig)
- runtime.configuredFrom = new(runtimeConfiguredFrom)
-
- // Set three fields not in the TOML config
- runtime.config.StateType = defaultRuntimeConfig.StateType
- runtime.config.OCIRuntime = defaultRuntimeConfig.OCIRuntime
- runtime.config.StorageConfig = storage.StoreOptions{}
-
- // Check to see if the given configuration file exists
- if _, err := os.Stat(configPath); err != nil {
- return nil, errors.Wrapf(err, "error checking existence of configuration file %s", configPath)
- }
-
- // Read contents of the config file
- contents, err := ioutil.ReadFile(configPath)
- if err != nil {
- return nil, errors.Wrapf(err, "error reading configuration file %s", configPath)
- }
-
- // Decode configuration file
- if _, err := toml.Decode(string(contents), runtime.config); err != nil {
- return nil, errors.Wrapf(err, "error decoding configuration from file %s", configPath)
- }
-
- // Overwrite the config with user-given configuration options
- for _, opt := range options {
- if err := opt(runtime); err != nil {
- return nil, errors.Wrapf(err, "error configuring runtime")
+ if !foundConfig {
+ os.MkdirAll(filepath.Dir(rootlessConfigPath), 0755)
+ file, err := os.OpenFile(rootlessConfigPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
+ if err != nil && !os.IsExist(err) {
+ return nil, errors.Wrapf(err, "cannot open file %s", rootlessConfigPath)
+ }
+ if err == nil {
+ defer file.Close()
+ enc := toml.NewEncoder(file)
+ if err := enc.Encode(runtime.config); err != nil {
+ os.Remove(rootlessConfigPath)
+ }
+ }
}
}
-
if err := makeRuntime(runtime); err != nil {
return nil, err
}
-
return runtime, nil
}
// Make a new runtime based on the given configuration
// Sets up containers/storage, state store, OCI runtime
func makeRuntime(runtime *Runtime) (err error) {
-
// Backward compatibility for `runtime_path`
if runtime.config.RuntimePath != nil {
// Don't print twice in rootless mode.
@@ -669,6 +696,8 @@ func makeRuntime(runtime *Runtime) (err error) {
runtime.config.VolumePath = dbConfig.VolumePath
}
+ runtime.config.EventsLogFilePath = filepath.Join(runtime.config.TmpDir, "events", "events.log")
+
logrus.Debugf("Using graph driver %s", runtime.config.StorageConfig.GraphDriverName)
logrus.Debugf("Using graph root %s", runtime.config.StorageConfig.GraphRoot)
logrus.Debugf("Using run root %s", runtime.config.StorageConfig.RunRoot)
@@ -711,6 +740,9 @@ func makeRuntime(runtime *Runtime) (err error) {
// Setting signaturepolicypath
ir.SignaturePolicyPath = runtime.config.SignaturePolicyPath
+ // Set logfile path for events
+ ir.EventsLogFilePath = runtime.config.EventsLogFilePath
+
defer func() {
if err != nil && store != nil {
// Don't forcibly shut down
@@ -743,6 +775,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
ociRuntime, err := newOCIRuntime(runtime.ociRuntimePath,
runtime.conmonPath, runtime.config.ConmonEnvVars,
@@ -948,9 +988,12 @@ func (r *Runtime) refreshRootless() error {
// Take advantage of a command that requires a new userns
// so that we are running as the root user and able to use refresh()
cmd := exec.Command(os.Args[0], "info")
- err := cmd.Run()
- if err != nil {
- return errors.Wrapf(err, "Error running %s info while refreshing state", os.Args[0])
+
+ if output, err := cmd.CombinedOutput(); err != nil {
+ if _, ok := err.(*exec.ExitError); !ok {
+ return errors.Wrapf(err, "Error waiting for info while refreshing state: %s", os.Args[0])
+ }
+ return errors.Wrapf(err, "Error running %s info while refreshing state: %s", os.Args[0], output)
}
return nil
}
diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go
index cfa4f9654..f23dc86dd 100644
--- a/libpod/runtime_ctr.go
+++ b/libpod/runtime_ctr.go
@@ -10,6 +10,7 @@ import (
"strings"
"time"
+ "github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/rootless"
"github.com/containers/storage"
@@ -170,7 +171,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
}()
if rootless.IsRootless() && ctr.config.ConmonPidFile == "" {
- ctr.config.ConmonPidFile = filepath.Join(ctr.state.RunDir, "conmon.pid")
+ ctr.config.ConmonPidFile = filepath.Join(ctr.config.StaticDir, "conmon.pid")
}
// Go through the volume mounts and check for named volumes
@@ -185,8 +186,11 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
return nil, errors.Wrapf(err, "error creating named volume %q", vol.Source)
}
ctr.config.Spec.Mounts[i].Source = newVol.MountPoint()
+ if err := os.Chown(ctr.config.Spec.Mounts[i].Source, ctr.RootUID(), ctr.RootGID()); err != nil {
+ return nil, errors.Wrapf(err, "cannot chown %q to %d:%d", ctr.config.Spec.Mounts[i].Source, ctr.RootUID(), ctr.RootGID())
+ }
if err := ctr.copyWithTarFromImage(ctr.config.Spec.Mounts[i].Destination, ctr.config.Spec.Mounts[i].Source); err != nil && !os.IsNotExist(err) {
- return nil, errors.Wrapf(err, "Failed to copy content into new volume mount %q", vol.Source)
+ return nil, errors.Wrapf(err, "failed to copy content into new volume mount %q", vol.Source)
}
continue
}
@@ -228,6 +232,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
return nil, err
}
}
+ ctr.newContainerEvent(events.Create)
return ctr, nil
}
@@ -239,7 +244,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 {
r.lock.Lock()
defer r.lock.Unlock()
-
return r.removeContainer(ctx, c, force, removeVolume)
}
@@ -430,6 +434,7 @@ func (r *Runtime) removeContainer(ctx context.Context, c *Container, force bool,
}
}
+ c.newContainerEvent(events.Remove)
return cleanupErr
}
diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go
index 451c2ebe7..02f925fc6 100644
--- a/libpod/runtime_img.go
+++ b/libpod/runtime_img.go
@@ -14,6 +14,7 @@ import (
"github.com/containers/libpod/pkg/util"
"github.com/containers/storage"
"github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
"github.com/containers/image/directory"
dockerarchive "github.com/containers/image/docker/archive"
@@ -183,6 +184,15 @@ func (r *Runtime) Import(ctx context.Context, source string, reference string, c
defer os.Remove(file)
source = file
}
+ // if it's stdin, buffer it, too
+ if source == "-" {
+ file, err := downloadFromFile(os.Stdin)
+ if err != nil {
+ return "", err
+ }
+ defer os.Remove(file)
+ source = file
+ }
newImage, err := r.imageRuntime.Import(ctx, source, reference, writer, image.SigningOptions{}, config)
if err != nil {
@@ -216,6 +226,25 @@ func downloadFromURL(source string) (string, error) {
return outFile.Name(), nil
}
+// donwloadFromFile reads all of the content from the reader and temporarily
+// saves in it /var/tmp/importxyz, which is deleted after the image is imported
+func downloadFromFile(reader *os.File) (string, error) {
+ outFile, err := ioutil.TempFile("/var/tmp", "import")
+ if err != nil {
+ return "", errors.Wrap(err, "error creating file")
+ }
+ defer outFile.Close()
+
+ logrus.Debugf("saving %s to %s", reader.Name(), outFile.Name())
+
+ _, err = io.Copy(outFile, reader)
+ if err != nil {
+ return "", errors.Wrapf(err, "error saving %s to %s", reader.Name(), outFile.Name())
+ }
+
+ return outFile.Name(), nil
+}
+
// LoadImage loads a container image into local storage
func (r *Runtime) LoadImage(ctx context.Context, name, inputFile string, writer io.Writer, signaturePolicy string) (string, error) {
var newImages []*image.Image
diff --git a/libpod/runtime_pod_infra_linux.go b/libpod/runtime_pod_infra_linux.go
index 4f221764a..0a5f78cf8 100644
--- a/libpod/runtime_pod_infra_linux.go
+++ b/libpod/runtime_pod_infra_linux.go
@@ -4,11 +4,15 @@ package libpod
import (
"context"
+ "strings"
"github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/rootless"
+ "github.com/opencontainers/image-spec/specs-go/v1"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
)
const (
@@ -17,7 +21,7 @@ const (
IDTruncLength = 12
)
-func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID string) (*Container, error) {
+func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID string, config *v1.ImageConfig) (*Container, error) {
// Set up generator for infra container defaults
g, err := generate.New("linux")
@@ -27,8 +31,44 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID
isRootless := rootless.IsRootless()
+ entryCmd := []string{r.config.InfraCommand}
+ // I've seen circumstances where config is being passed as nil.
+ // Let's err on the side of safety and make sure it's safe to use.
+ if config != nil {
+ setEntrypoint := false
+ // default to entrypoint in image if there is one
+ if len(config.Entrypoint) > 0 {
+ entryCmd = config.Entrypoint
+ setEntrypoint = true
+ }
+ if len(config.Cmd) > 0 {
+ // We can't use the default pause command, since we're
+ // sourcing from the image. If we didn't already set an
+ // entrypoint, set one now.
+ if !setEntrypoint {
+ // Use the Docker default "/bin/sh -c"
+ // entrypoint, as we're overriding command.
+ // If an image doesn't want this, it can
+ // override entrypoint too.
+ entryCmd = []string{"/bin/sh", "-c"}
+ }
+ entryCmd = append(entryCmd, config.Cmd...)
+ }
+ if len(config.Env) > 0 {
+ for _, nameValPair := range config.Env {
+ nameValSlice := strings.Split(nameValPair, "=")
+ if len(nameValSlice) < 2 {
+ return nil, errors.Errorf("Invalid environment variable structure in pause image")
+ }
+ g.AddProcessEnv(nameValSlice[0], nameValSlice[1])
+ }
+ }
+ }
+
g.SetRootReadonly(true)
- g.SetProcessArgs([]string{r.config.InfraCommand})
+ g.SetProcessArgs(entryCmd)
+
+ logrus.Debugf("Using %q as infra container entrypoint", entryCmd)
if isRootless {
g.RemoveMount("/dev/pts")
@@ -79,5 +119,5 @@ func (r *Runtime) createInfraContainer(ctx context.Context, p *Pod) (*Container,
imageName := newImage.Names()[0]
imageID := data.ID
- return r.makeInfraContainer(ctx, p, imageName, imageID)
+ return r.makeInfraContainer(ctx, p, imageName, imageID, data.Config)
}
diff --git a/libpod/runtime_pod_linux.go b/libpod/runtime_pod_linux.go
index c378d18e4..0011c771a 100644
--- a/libpod/runtime_pod_linux.go
+++ b/libpod/runtime_pod_linux.go
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/containerd/cgroups"
+ "github.com/containers/libpod/libpod/events"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@@ -95,9 +96,12 @@ func (r *Runtime) NewPod(ctx context.Context, options ...PodCreateOption) (*Pod,
if pod.config.UsePodCgroup {
logrus.Debugf("Got pod cgroup as %s", pod.state.CgroupPath)
}
- if pod.HasInfraContainer() != pod.SharesNamespaces() {
+ if !pod.HasInfraContainer() && pod.SharesNamespaces() {
return nil, errors.Errorf("Pods must have an infra container to share namespaces")
}
+ if pod.HasInfraContainer() && !pod.SharesNamespaces() {
+ logrus.Warnf("Pod has an infra container, but shares no namespaces")
+ }
if err := r.state.AddPod(pod); err != nil {
return nil, errors.Wrapf(err, "error adding pod to state")
@@ -118,7 +122,7 @@ func (r *Runtime) NewPod(ctx context.Context, options ...PodCreateOption) (*Pod,
return nil, err
}
}
-
+ pod.newPodEvent(events.Create)
return pod, nil
}
@@ -304,6 +308,6 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool)
// Mark pod invalid
p.valid = false
-
+ p.newPodEvent(events.Remove)
return nil
}
diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go
index 11f37ad4b..68c6c107e 100644
--- a/libpod/runtime_volume.go
+++ b/libpod/runtime_volume.go
@@ -2,9 +2,11 @@ package libpod
import (
"context"
+ "strings"
+
+ "github.com/containers/libpod/libpod/events"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
- "strings"
)
// 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 r.removeVolume(ctx, v, force)
}
@@ -171,6 +172,7 @@ func (r *Runtime) PruneVolumes(ctx context.Context) ([]string, []error) {
}
continue
}
+ vol.newVolumeEvent(events.Prune)
prunedIDs = append(prunedIDs, vol.Name())
}
return prunedIDs, pruneErrors
diff --git a/libpod/runtime_volume_linux.go b/libpod/runtime_volume_linux.go
index 838c0167a..b51bb8213 100644
--- a/libpod/runtime_volume_linux.go
+++ b/libpod/runtime_volume_linux.go
@@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
+ "github.com/containers/libpod/libpod/events"
"github.com/containers/storage/pkg/stringid"
"github.com/opencontainers/selinux/go-selinux/label"
"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 {
return nil, errors.Wrapf(err, "error adding volume to state")
}
-
+ defer volume.newVolumeEvent(events.Create)
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())
}
+ defer v.newVolumeEvent(events.Remove)
logrus.Debugf("Removed volume %s", v.Name())
-
return nil
}
diff --git a/libpod/runtime_volume_unsupported.go b/libpod/runtime_volume_unsupported.go
index d87459759..5fe487114 100644
--- a/libpod/runtime_volume_unsupported.go
+++ b/libpod/runtime_volume_unsupported.go
@@ -6,7 +6,7 @@ import (
"context"
)
-func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force, prune bool) error {
+func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool) error {
return ErrNotImplemented
}