diff options
Diffstat (limited to 'libpod')
-rw-r--r-- | libpod/container.go | 7 | ||||
-rw-r--r-- | libpod/container_config.go | 4 | ||||
-rw-r--r-- | libpod/container_inspect.go | 27 | ||||
-rw-r--r-- | libpod/container_internal.go | 38 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 6 | ||||
-rw-r--r-- | libpod/events.go | 5 | ||||
-rw-r--r-- | libpod/events/config.go | 4 | ||||
-rw-r--r-- | libpod/events/events.go | 21 | ||||
-rw-r--r-- | libpod/events/logfile.go | 156 | ||||
-rw-r--r-- | libpod/events/logfile_test.go | 140 | ||||
-rw-r--r-- | libpod/options.go | 24 | ||||
-rw-r--r-- | libpod/runtime_ctr.go | 4 |
12 files changed, 381 insertions, 55 deletions
diff --git a/libpod/container.go b/libpod/container.go index 578f16905..bc3cab439 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -291,6 +291,13 @@ func (c *Container) Config() *ContainerConfig { return returnConfig } +// ConfigNoCopy returns the configuration used by the container. +// Note that the returned value is not a copy and must hence +// only be used in a reading fashion. +func (c *Container) ConfigNoCopy() *ContainerConfig { + return c.config +} + // DeviceHostSrc returns the user supplied device to be passed down in the pod func (c *Container) DeviceHostSrc() []spec.LinuxDevice { return c.config.DeviceHostSrc diff --git a/libpod/container_config.go b/libpod/container_config.go index 8500c6db9..371a1dec0 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -120,6 +120,10 @@ type ContainerRootFSConfig struct { // with the size specified in ShmSize and populate this with the path of // said tmpfs. ShmDir string `json:"ShmDir,omitempty"` + // NoShmShare indicates whether /dev/shm can be shared with other containers + NoShmShare bool `json:"NOShmShare,omitempty"` + // NoShm indicates whether a tmpfs should be created and mounted on /dev/shm + NoShm bool `json:"NoShm,omitempty"` // ShmSize is the size of the container's SHM. Only used if ShmDir was // not set manually at time of creation. ShmSize int64 `json:"shmSize"` diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index c9d0b8a6c..14290ca0d 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -703,32 +703,31 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named } hostConfig.CapAdd = capAdd hostConfig.CapDrop = capDrop - - // IPC Namespace mode - ipcMode := "" - if c.config.IPCNsCtr != "" { - ipcMode = fmt.Sprintf("container:%s", c.config.IPCNsCtr) - } else if ctrSpec.Linux != nil { + switch { + case c.config.IPCNsCtr != "": + hostConfig.IpcMode = fmt.Sprintf("container:%s", c.config.IPCNsCtr) + case ctrSpec.Linux != nil: // Locate the spec's IPC namespace. // If there is none, it's ipc=host. // If there is one and it has a path, it's "ns:". // If no path, it's default - the empty string. - for _, ns := range ctrSpec.Linux.Namespaces { if ns.Type == spec.IPCNamespace { if ns.Path != "" { - ipcMode = fmt.Sprintf("ns:%s", ns.Path) + hostConfig.IpcMode = fmt.Sprintf("ns:%s", ns.Path) } else { - ipcMode = "private" + break } - break } } - if ipcMode == "" { - ipcMode = "host" - } + case c.config.NoShm: + hostConfig.IpcMode = "none" + case c.config.NoShmShare: + hostConfig.IpcMode = "private" + } + if hostConfig.IpcMode == "" { + hostConfig.IpcMode = "shareable" } - hostConfig.IpcMode = ipcMode // Cgroup namespace mode cgroupMode := "" diff --git a/libpod/container_internal.go b/libpod/container_internal.go index f1f467879..c7567a55e 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -1507,26 +1507,28 @@ func (c *Container) mountStorage() (_ string, deferredErr error) { return c.state.Mountpoint, nil } - mounted, err := mount.Mounted(c.config.ShmDir) - if err != nil { - return "", errors.Wrapf(err, "unable to determine if %q is mounted", c.config.ShmDir) - } - - if !mounted && !MountExists(c.config.Spec.Mounts, "/dev/shm") { - shmOptions := fmt.Sprintf("mode=1777,size=%d", c.config.ShmSize) - if err := c.mountSHM(shmOptions); err != nil { - return "", err - } - if err := os.Chown(c.config.ShmDir, c.RootUID(), c.RootGID()); err != nil { - return "", errors.Wrapf(err, "failed to chown %s", c.config.ShmDir) + if !c.config.NoShm { + mounted, err := mount.Mounted(c.config.ShmDir) + if err != nil { + return "", errors.Wrapf(err, "unable to determine if %q is mounted", c.config.ShmDir) } - defer func() { - if deferredErr != nil { - if err := c.unmountSHM(c.config.ShmDir); err != nil { - logrus.Errorf("Unmounting SHM for container %s after mount error: %v", c.ID(), err) - } + + if !mounted && !MountExists(c.config.Spec.Mounts, "/dev/shm") { + shmOptions := fmt.Sprintf("mode=1777,size=%d", c.config.ShmSize) + if err := c.mountSHM(shmOptions); err != nil { + return "", err } - }() + if err := os.Chown(c.config.ShmDir, c.RootUID(), c.RootGID()); err != nil { + return "", errors.Wrapf(err, "failed to chown %s", c.config.ShmDir) + } + defer func() { + if deferredErr != nil { + if err := c.unmountSHM(c.config.ShmDir); err != nil { + logrus.Errorf("Unmounting SHM for container %s after mount error: %v", c.ID(), err) + } + } + }() + } } // We need to mount the container before volumes - to ensure the copyup diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 9369b746c..9991003d6 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -1963,8 +1963,10 @@ func (c *Container) makeBindMounts() error { } } - // SHM is always added when we mount the container - c.state.BindMounts["/dev/shm"] = c.config.ShmDir + if c.config.ShmDir != "" { + // If ShmDir has a value SHM is always added when we mount the container + c.state.BindMounts["/dev/shm"] = c.config.ShmDir + } if c.config.Passwd == nil || *c.config.Passwd { newPasswd, newGroup, err := c.generatePasswdAndGroup() diff --git a/libpod/events.go b/libpod/events.go index d6595180a..3908536a1 100644 --- a/libpod/events.go +++ b/libpod/events.go @@ -13,8 +13,9 @@ import ( // newEventer returns an eventer that can be used to read/write events func (r *Runtime) newEventer() (events.Eventer, error) { options := events.EventerOptions{ - EventerType: r.config.Engine.EventsLogger, - LogFilePath: r.config.Engine.EventsLogFilePath, + EventerType: r.config.Engine.EventsLogger, + LogFilePath: r.config.Engine.EventsLogFilePath, + LogFileMaxSize: r.config.Engine.EventsLogFileMaxSize, } return events.NewEventer(options) } diff --git a/libpod/events/config.go b/libpod/events/config.go index 188d15578..35680a275 100644 --- a/libpod/events/config.go +++ b/libpod/events/config.go @@ -60,6 +60,8 @@ type EventerOptions struct { // LogFilePath is the path to where the log file should reside if using // the file logger LogFilePath string + // LogFileMaxSize is the default limit used for rotating the log file + LogFileMaxSize uint64 } // Eventer is the interface for journald or file event logging @@ -171,6 +173,8 @@ const ( Restart Status = "restart" // Restore ... Restore Status = "restore" + // Rotate indicates that the log file was rotated + Rotate Status = "log-rotation" // Save ... Save Status = "save" // Start ... diff --git a/libpod/events/events.go b/libpod/events/events.go index 2cdd2ab67..1745095fb 100644 --- a/libpod/events/events.go +++ b/libpod/events/events.go @@ -3,11 +3,9 @@ package events import ( "encoding/json" "fmt" - "os" "time" "github.com/containers/storage/pkg/stringid" - "github.com/nxadm/tail" "github.com/pkg/errors" ) @@ -87,7 +85,11 @@ func (e *Event) ToHumanReadable(truncate bool) string { case Image: humanFormat = fmt.Sprintf("%s %s %s %s %s", e.Time, e.Type, e.Status, id, e.Name) case System: - humanFormat = fmt.Sprintf("%s %s %s", e.Time, e.Type, e.Status) + if e.Name != "" { + humanFormat = fmt.Sprintf("%s %s %s %s", e.Time, e.Type, e.Status, e.Name) + } else { + humanFormat = fmt.Sprintf("%s %s %s", e.Time, e.Type, e.Status) + } case Volume: humanFormat = fmt.Sprintf("%s %s %s %s", e.Time, e.Type, e.Status, e.Name) } @@ -196,6 +198,8 @@ func StringToStatus(name string) (Status, error) { return Restart, nil case Restore.String(): return Restore, nil + case Rotate.String(): + return Rotate, nil case Save.String(): return Save, nil case Start.String(): @@ -215,14 +219,3 @@ func StringToStatus(name string) (Status, error) { } return "", errors.Errorf("unknown event status %q", name) } - -func (e EventLogFile) getTail(options ReadOptions) (*tail.Tail, error) { - reopen := true - seek := tail.SeekInfo{Offset: 0, Whence: os.SEEK_END} - if options.FromStart || !options.Stream { - seek.Whence = 0 - reopen = false - } - stream := options.Stream - return tail.TailFile(e.options.LogFilePath, tail.Config{ReOpen: reopen, Follow: stream, Location: &seek, Logger: tail.DiscardingLogger, Poll: true}) -} diff --git a/libpod/events/logfile.go b/libpod/events/logfile.go index 76173cde9..5091f3723 100644 --- a/libpod/events/logfile.go +++ b/libpod/events/logfile.go @@ -1,15 +1,24 @@ +//go:build linux +// +build linux + package events import ( + "bufio" "context" "fmt" + "io" + "io/ioutil" "os" + "path" "time" "github.com/containers/podman/v4/pkg/util" "github.com/containers/storage/pkg/lockfile" + "github.com/nxadm/tail" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" ) // EventLogFile is the structure for event writing to a logfile. It contains the eventer @@ -27,21 +36,55 @@ func (e EventLogFile) Write(ee Event) error { } lock.Lock() defer lock.Unlock() - f, err := os.OpenFile(e.options.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700) + + eventJSONString, err := ee.ToJSONString() if err != nil { return err } - defer f.Close() - eventJSONString, err := ee.ToJSONString() + + rotated, err := rotateLog(e.options.LogFilePath, eventJSONString, e.options.LogFileMaxSize) + if err != nil { + return fmt.Errorf("rotating log file: %w", err) + } + + if rotated { + rEvent := NewEvent(Rotate) + rEvent.Type = System + rEvent.Name = e.options.LogFilePath + rotateJSONString, err := rEvent.ToJSONString() + if err != nil { + return err + } + if err := e.writeString(rotateJSONString); err != nil { + return err + } + } + + return e.writeString(eventJSONString) +} + +func (e EventLogFile) writeString(s string) error { + f, err := os.OpenFile(e.options.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700) if err != nil { return err } - if _, err := f.WriteString(fmt.Sprintf("%s\n", eventJSONString)); err != nil { + if _, err := f.WriteString(s + "\n"); err != nil { return err } return nil } +func (e EventLogFile) getTail(options ReadOptions) (*tail.Tail, error) { + reopen := true + seek := tail.SeekInfo{Offset: 0, Whence: os.SEEK_END} + if options.FromStart || !options.Stream { + seek.Whence = 0 + reopen = false + } + stream := options.Stream + return tail.TailFile(e.options.LogFilePath, tail.Config{ReOpen: reopen, Follow: stream, Location: &seek, Logger: tail.DiscardingLogger, Poll: true}) +} + // Reads from the log file func (e EventLogFile) Read(ctx context.Context, options ReadOptions) error { defer close(options.EventChannel) @@ -107,3 +150,108 @@ func (e EventLogFile) Read(ctx context.Context, options ReadOptions) error { func (e EventLogFile) String() string { return LogFile.String() } + +// Rotates the log file if the log file size and content exceeds limit +func rotateLog(logfile string, content string, limit uint64) (bool, error) { + if limit == 0 { + return false, nil + } + file, err := os.Stat(logfile) + if err != nil { + return false, err + } + var filesize = uint64(file.Size()) + var contentsize = uint64(len([]rune(content))) + if filesize+contentsize < limit { + return false, nil + } + + if err := truncate(logfile); err != nil { + return false, err + } + return true, nil +} + +// Truncates the log file and saves 50% of content to new log file +func truncate(filePath string) error { + orig, err := os.Open(filePath) + if err != nil { + return err + } + defer orig.Close() + + origFinfo, err := orig.Stat() + if err != nil { + return err + } + + size := origFinfo.Size() + threshold := size / 2 + + tmp, err := ioutil.TempFile(path.Dir(filePath), "") + if err != nil { + // Retry in /tmp in case creating a tmp file in the same + // directory has failed. + tmp, err = ioutil.TempFile("", "") + if err != nil { + return err + } + } + defer tmp.Close() + + // Jump directly to the threshold, drop the first line and copy the remainder + if _, err := orig.Seek(threshold, 0); err != nil { + return err + } + reader := bufio.NewReader(orig) + if _, err := reader.ReadString('\n'); err != nil { + if !errors.Is(err, io.EOF) { + return err + } + } + if _, err := reader.WriteTo(tmp); err != nil { + return fmt.Errorf("writing truncated contents: %w", err) + } + + if err := renameLog(tmp.Name(), filePath); err != nil { + return fmt.Errorf("writing back %s to %s: %w", tmp.Name(), filePath, err) + } + + return nil +} + +// Renames from, to +func renameLog(from, to string) error { + err := os.Rename(from, to) + if err == nil { + return nil + } + + if !errors.Is(err, unix.EXDEV) { + return err + } + + // Files are not on the same partition, so we need to copy them the + // hard way. + fFrom, err := os.Open(from) + if err != nil { + return err + } + defer fFrom.Close() + + fTo, err := os.Create(to) + if err != nil { + return err + } + defer fTo.Close() + + if _, err := io.Copy(fTo, fFrom); err != nil { + return fmt.Errorf("writing back from temporary file: %w", err) + } + + if err := os.Remove(from); err != nil { + return fmt.Errorf("removing temporary file: %w", err) + } + + return nil +} diff --git a/libpod/events/logfile_test.go b/libpod/events/logfile_test.go new file mode 100644 index 000000000..302533c12 --- /dev/null +++ b/libpod/events/logfile_test.go @@ -0,0 +1,140 @@ +package events + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRotateLog(t *testing.T) { + tests := []struct { + // If sizeInitial + sizeContent >= sizeLimit, then rotate + sizeInitial uint64 + sizeContent uint64 + sizeLimit uint64 + mustRotate bool + }{ + // No rotation + {0, 0, 1, false}, + {1, 1, 0, false}, + {10, 10, 30, false}, + {1000, 500, 1600, false}, + // Rotation + {10, 10, 20, true}, + {30, 0, 29, true}, + {200, 50, 150, true}, + {1000, 600, 1500, true}, + } + + for _, test := range tests { + tmp, err := ioutil.TempFile("", "log-rotation-") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + defer tmp.Close() + + // Create dummy file and content. + initialContent := make([]byte, test.sizeInitial) + logContent := make([]byte, test.sizeContent) + + // Write content to the file. + _, err = tmp.Write(initialContent) + require.NoError(t, err) + + // Now rotate + fInfoBeforeRotate, err := tmp.Stat() + require.NoError(t, err) + isRotated, err := rotateLog(tmp.Name(), string(logContent), test.sizeLimit) + require.NoError(t, err) + + fInfoAfterRotate, err := os.Stat(tmp.Name()) + // Test if rotation was successful + if test.mustRotate { + // File has been renamed + require.True(t, isRotated) + require.NoError(t, err, "log file has been renamed") + require.NotEqual(t, fInfoBeforeRotate.Size(), fInfoAfterRotate.Size()) + } else { + // File has not been renamed + require.False(t, isRotated) + require.NoError(t, err, "log file has not been renamed") + require.Equal(t, fInfoBeforeRotate.Size(), fInfoAfterRotate.Size()) + } + } +} + +func TestTruncationOutput(t *testing.T) { + contentBefore := `0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +` + contentAfter := `6 +7 +8 +9 +10 +` + // Create dummy file + tmp, err := ioutil.TempFile("", "log-rotation") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + defer tmp.Close() + + // Write content before truncation to dummy file + _, err = tmp.WriteString(contentBefore) + require.NoError(t, err) + + // Truncate the file + beforeTruncation, err := ioutil.ReadFile(tmp.Name()) + require.NoError(t, err) + err = truncate(tmp.Name()) + require.NoError(t, err) + afterTruncation, err := ioutil.ReadFile(tmp.Name()) + require.NoError(t, err) + + // Test if rotation was successful + require.NoError(t, err, "Log content has changed") + require.NotEqual(t, beforeTruncation, afterTruncation) + require.Equal(t, string(afterTruncation), contentAfter) +} + +func TestRenameLog(t *testing.T) { + fileContent := `0 +1 +2 +3 +4 +5 +` + // Create two dummy files + source, err := ioutil.TempFile("", "removing") + require.NoError(t, err) + target, err := ioutil.TempFile("", "renaming") + require.NoError(t, err) + + // Write to source dummy file + _, err = source.WriteString(fileContent) + require.NoError(t, err) + + // Rename the files + beforeRename, err := ioutil.ReadFile(source.Name()) + require.NoError(t, err) + err = renameLog(source.Name(), target.Name()) + require.NoError(t, err) + afterRename, err := ioutil.ReadFile(target.Name()) + require.NoError(t, err) + + // Test if renaming was successful + require.Error(t, os.Remove(source.Name())) + require.NoError(t, os.Remove(target.Name())) + require.Equal(t, beforeRename, afterRename) +} diff --git a/libpod/options.go b/libpod/options.go index 6c4b4cc42..ffd0e6037 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -559,6 +559,30 @@ func WithShmDir(dir string) CtrCreateOption { } } +// WithNOShmMount tells libpod whether to mount /dev/shm +func WithNoShm(mount bool) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + + ctr.config.NoShm = mount + return nil + } +} + +// WithNoShmShare tells libpod whether to share containers /dev/shm with other containers +func WithNoShmShare(share bool) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + + ctr.config.NoShmShare = share + return nil + } +} + // WithSystemd turns on systemd mode in the container func WithSystemd() CtrCreateOption { return func(ctr *Container) error { diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index f92898b1c..7edd49fd1 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -174,6 +174,8 @@ func (r *Runtime) initContainerVariables(rSpec *spec.Spec, config *ContainerConf return nil, errors.Wrapf(err, "converting containers.conf ShmSize %s to an int", r.config.Containers.ShmSize) } ctr.config.ShmSize = size + ctr.config.NoShm = false + ctr.config.NoShmShare = false ctr.config.StopSignal = 15 ctr.config.StopTimeout = r.config.Engine.StopTimeout @@ -514,7 +516,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai } } - if !MountExists(ctr.config.Spec.Mounts, "/dev/shm") && ctr.config.ShmDir == "" { + if !MountExists(ctr.config.Spec.Mounts, "/dev/shm") && ctr.config.ShmDir == "" && !ctr.config.NoShm { ctr.config.ShmDir = filepath.Join(ctr.bundlePath(), "shm") if err := os.MkdirAll(ctr.config.ShmDir, 0700); err != nil { if !os.IsExist(err) { |