diff options
Diffstat (limited to 'libpod')
-rw-r--r-- | libpod/boltdb_state_internal.go | 13 | ||||
-rw-r--r-- | libpod/container.go | 19 | ||||
-rw-r--r-- | libpod/container_copy_linux.go | 12 | ||||
-rw-r--r-- | libpod/container_internal.go | 42 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 71 | ||||
-rw-r--r-- | libpod/container_internal_linux_test.go | 8 | ||||
-rw-r--r-- | libpod/container_internal_test.go | 12 | ||||
-rw-r--r-- | libpod/define/info.go | 25 | ||||
-rw-r--r-- | libpod/define/pod_inspect.go | 2 | ||||
-rw-r--r-- | libpod/events/config.go | 6 | ||||
-rw-r--r-- | libpod/events/events.go | 22 | ||||
-rw-r--r-- | libpod/events/events_linux.go | 2 | ||||
-rw-r--r-- | libpod/events/memory.go | 49 | ||||
-rw-r--r-- | libpod/info.go | 75 | ||||
-rw-r--r-- | libpod/info_test.go | 59 | ||||
-rw-r--r-- | libpod/kube.go | 6 | ||||
-rw-r--r-- | libpod/lock/file/file_lock_test.go | 11 | ||||
-rw-r--r-- | libpod/networking_linux.go | 2 | ||||
-rw-r--r-- | libpod/oci_attach_linux.go | 11 | ||||
-rw-r--r-- | libpod/options.go | 18 | ||||
-rw-r--r-- | libpod/pod.go | 4 | ||||
-rw-r--r-- | libpod/pod_api.go | 51 | ||||
-rw-r--r-- | libpod/runtime.go | 28 | ||||
-rw-r--r-- | libpod/runtime_ctr.go | 4 | ||||
-rw-r--r-- | libpod/runtime_pod_linux.go | 15 | ||||
-rw-r--r-- | libpod/runtime_worker.go | 41 | ||||
-rw-r--r-- | libpod/util.go | 7 |
27 files changed, 511 insertions, 104 deletions
diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go index e43226490..d6f035af9 100644 --- a/libpod/boltdb_state_internal.go +++ b/libpod/boltdb_state_internal.go @@ -542,8 +542,12 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error { ctr.ID(), s.namespace, ctr.config.Namespace) } + // Set the original networks to nil. We can save some space by not storing it in the config + // since we store it in a different mutable bucket anyway. + configNetworks := ctr.config.Networks + ctr.config.Networks = nil + // JSON container structs to insert into DB - // TODO use a higher-performance struct encoding than JSON configJSON, err := json.Marshal(ctr.config) if err != nil { return errors.Wrapf(err, "error marshalling container %s config to JSON", ctr.ID()) @@ -564,8 +568,8 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error { } // make sure to marshal the network options before we get the db lock - networks := make(map[string][]byte, len(ctr.config.Networks)) - for net, opts := range ctr.config.Networks { + networks := make(map[string][]byte, len(configNetworks)) + for net, opts := range configNetworks { // Check that we don't have any empty network names if net == "" { return errors.Wrapf(define.ErrInvalidArg, "network names cannot be an empty string") @@ -581,9 +585,6 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error { } networks[net] = optBytes } - // Set the original value to nil. We can safe some space by not storing it in the config - // since we store it in a different mutable bucket anyway. - ctr.config.Networks = nil db, err := s.getDBCon() if err != nil { diff --git a/libpod/container.go b/libpod/container.go index 3e7ab7b0a..4b2af02ab 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -281,12 +281,29 @@ type ContainerNetworkDescriptions map[string]int // Config accessors // Unlocked -// Config returns the configuration used to create the container +// Config returns the configuration used to create the container. +// Note that the returned config does not include the actual networks. +// Use ConfigWithNetworks() if you need them. func (c *Container) Config() *ContainerConfig { returnConfig := new(ContainerConfig) if err := JSONDeepCopy(c.config, returnConfig); err != nil { return nil } + return returnConfig +} + +// Config returns the configuration used to create the container. +func (c *Container) ConfigWithNetworks() *ContainerConfig { + returnConfig := c.Config() + if returnConfig == nil { + return nil + } + + networks, err := c.networks() + if err != nil { + return nil + } + returnConfig.Networks = networks return returnConfig } diff --git a/libpod/container_copy_linux.go b/libpod/container_copy_linux.go index 91e712c74..7566fbb12 100644 --- a/libpod/container_copy_linux.go +++ b/libpod/container_copy_linux.go @@ -48,7 +48,11 @@ func (c *Container) copyFromArchive(path string, chown bool, rename map[string]s if err != nil { return nil, err } - unmount = func() { c.unmount(false) } + unmount = func() { + if err := c.unmount(false); err != nil { + logrus.Errorf("Failed to unmount container: %v", err) + } + } } if c.state.State == define.ContainerStateRunning { @@ -117,7 +121,11 @@ func (c *Container) copyToArchive(path string, writer io.Writer) (func() error, if err != nil { return nil, err } - unmount = func() { c.unmount(false) } + unmount = func() { + if err := c.unmount(false); err != nil { + logrus.Errorf("Failed to unmount container: %v", err) + } + } } statInfo, resolvedRoot, resolvedPath, err := c.stat(mountPoint, path) diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 5c6719bdf..7494eb3ec 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -1939,9 +1939,51 @@ func (c *Container) cleanup(ctx context.Context) error { } } + if err := c.stopPodIfNeeded(context.Background()); err != nil { + if lastError == nil { + lastError = err + } else { + logrus.Errorf("Stopping pod of container %s: %v", c.ID(), err) + } + } + return lastError } +// If the container is part of a pod where only the infra container remains +// running, attempt to stop the pod. +func (c *Container) stopPodIfNeeded(ctx context.Context) error { + if c.config.Pod == "" { + return nil + } + + pod, err := c.runtime.state.Pod(c.config.Pod) + if err != nil { + return fmt.Errorf("container %s is in pod %s, but pod cannot be retrieved: %w", c.ID(), c.config.Pod, err) + } + + switch pod.config.ExitPolicy { + case config.PodExitPolicyContinue: + return nil + + case config.PodExitPolicyStop: + // Use the runtime's work queue to stop the pod. This resolves + // a number of scenarios where we'd otherwise run into + // deadlocks. For instance, during `pod stop`, the pod has + // already been locked. + // The work queue is a simple means without having to worry about + // future changes that may introduce more deadlock scenarios. + c.runtime.queueWork(func() { + if err := pod.stopIfOnlyInfraRemains(ctx, c.ID()); err != nil { + if !errors.Is(err, define.ErrNoSuchPod) { + logrus.Errorf("Checking if infra needs to be stopped: %v", err) + } + } + }) + } + return nil +} + // delete deletes the container and runs any configured poststop // hooks. func (c *Container) delete(ctx context.Context) error { diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 31edff762..4742b22ab 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -1180,7 +1180,11 @@ func (c *Container) createCheckpointImage(ctx context.Context, options Container return err } // Clean-up buildah working container - defer importBuilder.Delete() + defer func() { + if err := importBuilder.Delete(); err != nil { + logrus.Errorf("Image builder delete failed: %v", err) + } + }() if err := c.prepareCheckpointExport(); err != nil { return err @@ -1201,7 +1205,9 @@ func (c *Container) createCheckpointImage(ctx context.Context, options Container // Copy checkpoint from temporary tar file in the image addAndCopyOptions := buildah.AddAndCopyOptions{} - importBuilder.Add("", true, addAndCopyOptions, options.TargetFile) + if err := importBuilder.Add("", true, addAndCopyOptions, options.TargetFile); err != nil { + return err + } if err := c.addCheckpointImageMetadata(importBuilder); err != nil { return err @@ -1543,7 +1549,11 @@ func (c *Container) importCheckpointImage(ctx context.Context, imageID string) e } mountPoint, err := img.Mount(ctx, nil, "") - defer img.Unmount(true) + defer func() { + if err := c.unmount(true); err != nil { + logrus.Errorf("Failed to unmount container: %v", err) + } + }() if err != nil { return err } @@ -2113,15 +2123,9 @@ func (c *Container) makeBindMounts() error { } } else { if !c.config.UseImageResolvConf { - newResolv, err := c.generateResolvConf() - if err != nil { + if err := c.generateResolvConf(); err != nil { return errors.Wrapf(err, "error creating resolv.conf for container %s", c.ID()) } - err = c.mountIntoRootDirs("/etc/resolv.conf", newResolv) - - if err != nil { - return errors.Wrapf(err, "error assigning mounts to container %s", c.ID()) - } } if !c.config.UseImageHosts { @@ -2278,23 +2282,25 @@ rootless=%d } // generateResolvConf generates a containers resolv.conf -func (c *Container) generateResolvConf() (string, error) { +func (c *Container) generateResolvConf() error { var ( nameservers []string networkNameServers []string networkSearchDomains []string ) + hostns := true resolvConf := "/etc/resolv.conf" for _, namespace := range c.config.Spec.Linux.Namespaces { if namespace.Type == spec.NetworkNamespace { + hostns = false if namespace.Path != "" && !strings.HasPrefix(namespace.Path, "/proc/") { definedPath := filepath.Join("/etc/netns", filepath.Base(namespace.Path), "resolv.conf") _, err := os.Stat(definedPath) if err == nil { resolvConf = definedPath } else if !os.IsNotExist(err) { - return "", err + return err } } break @@ -2304,17 +2310,17 @@ func (c *Container) generateResolvConf() (string, error) { contents, err := ioutil.ReadFile(resolvConf) // resolv.conf doesn't have to exists if err != nil && !os.IsNotExist(err) { - return "", err + return err } ns := resolvconf.GetNameservers(contents) // check if systemd-resolved is used, assume it is used when 127.0.0.53 is the only nameserver - if len(ns) == 1 && ns[0] == "127.0.0.53" { + if !hostns && len(ns) == 1 && ns[0] == "127.0.0.53" { // read the actual resolv.conf file for systemd-resolved resolvedContents, err := ioutil.ReadFile("/run/systemd/resolve/resolv.conf") if err != nil { if !os.IsNotExist(err) { - return "", errors.Wrapf(err, "detected that systemd-resolved is in use, but could not locate real resolv.conf") + return errors.Wrapf(err, "detected that systemd-resolved is in use, but could not locate real resolv.conf") } } else { contents = resolvedContents @@ -2337,21 +2343,21 @@ func (c *Container) generateResolvConf() (string, error) { ipv6, err := c.checkForIPv6(netStatus) if err != nil { - return "", err + return err } // Ensure that the container's /etc/resolv.conf is compatible with its // network configuration. - resolv, err := resolvconf.FilterResolvDNS(contents, ipv6, c.config.CreateNetNS) + resolv, err := resolvconf.FilterResolvDNS(contents, ipv6, !hostns) if err != nil { - return "", errors.Wrapf(err, "error parsing host resolv.conf") + return errors.Wrapf(err, "error parsing host resolv.conf") } dns := make([]net.IP, 0, len(c.runtime.config.Containers.DNSServers)+len(c.config.DNSServer)) for _, i := range c.runtime.config.Containers.DNSServers { result := net.ParseIP(i) if result == nil { - return "", errors.Wrapf(define.ErrInvalidArg, "invalid IP address %s", i) + return errors.Wrapf(define.ErrInvalidArg, "invalid IP address %s", i) } dns = append(dns, result) } @@ -2402,20 +2408,15 @@ func (c *Container) generateResolvConf() (string, error) { destPath := filepath.Join(c.state.RunDir, "resolv.conf") if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) { - return "", errors.Wrapf(err, "container %s", c.ID()) + return errors.Wrapf(err, "container %s", c.ID()) } // Build resolv.conf if _, err = resolvconf.Build(destPath, nameservers, search, options); err != nil { - return "", errors.Wrapf(err, "error building resolv.conf for container %s", c.ID()) + return errors.Wrapf(err, "error building resolv.conf for container %s", c.ID()) } - // Relabel resolv.conf for the container - if err := c.relabel(destPath, c.config.MountLabel, true); err != nil { - return "", err - } - - return destPath, nil + return c.bindMountRootFile(destPath, "/etc/resolv.conf") } // Check if a container uses IPv6. @@ -2590,17 +2591,21 @@ func (c *Container) createHosts() error { return err } - if err := os.Chown(targetFile, c.RootUID(), c.RootGID()); err != nil { + return c.bindMountRootFile(targetFile, config.DefaultHostsFile) +} + +// bindMountRootFile will chown and relabel the source file to make it usable in the container. +// It will also add the path to the container bind mount map. +// source is the path on the host, dest is the path in the container. +func (c *Container) bindMountRootFile(source, dest string) error { + if err := os.Chown(source, c.RootUID(), c.RootGID()); err != nil { return err } - if err := label.Relabel(targetFile, c.MountLabel(), false); err != nil { + if err := label.Relabel(source, c.MountLabel(), false); err != nil { return err } - if err = c.mountIntoRootDirs(config.DefaultHostsFile, targetFile); err != nil { - return err - } - return nil + return c.mountIntoRootDirs(dest, source) } // generateGroupEntry generates an entry or entries into /etc/group as diff --git a/libpod/container_internal_linux_test.go b/libpod/container_internal_linux_test.go index 03095aa58..ed1f63a31 100644 --- a/libpod/container_internal_linux_test.go +++ b/libpod/container_internal_linux_test.go @@ -4,8 +4,6 @@ package libpod import ( - "io/ioutil" - "os" "testing" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -13,12 +11,6 @@ import ( ) func TestGenerateUserPasswdEntry(t *testing.T) { - dir, err := ioutil.TempDir("", "libpod_test_") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - c := Container{ config: &ContainerConfig{ Spec: &spec.Spec{}, diff --git a/libpod/container_internal_test.go b/libpod/container_internal_test.go index 2b50093b2..1b4e62e91 100644 --- a/libpod/container_internal_test.go +++ b/libpod/container_internal_test.go @@ -4,10 +4,8 @@ import ( "context" "fmt" "io/ioutil" - "os" "path/filepath" "runtime" - "strings" "testing" rspec "github.com/opencontainers/runtime-spec/specs-go" @@ -19,11 +17,7 @@ var hookPath string func TestPostDeleteHooks(t *testing.T) { ctx := context.Background() - dir, err := ioutil.TempDir("", "libpod_test_") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() statePath := filepath.Join(dir, "state") copyPath := filepath.Join(dir, "copy") @@ -57,12 +51,12 @@ func TestPostDeleteHooks(t *testing.T) { }, }, } - err = c.postDeleteHooks(ctx) + err := c.postDeleteHooks(ctx) if err != nil { t.Fatal(err) } - stateRegexp := `{"ociVersion":"1\.0\.2-dev","id":"123abc","status":"stopped","bundle":"` + strings.TrimSuffix(os.TempDir(), "/") + `/libpod_test_[0-9]*","annotations":{"a":"b"}}` + stateRegexp := `{"ociVersion":"1\.0\.2-dev","id":"123abc","status":"stopped","bundle":"` + dir + `","annotations":{"a":"b"}}` for _, p := range []string{statePath, copyPath} { path := p t.Run(path, func(t *testing.T) { diff --git a/libpod/define/info.go b/libpod/define/info.go index 713129ada..911fa5c03 100644 --- a/libpod/define/info.go +++ b/libpod/define/info.go @@ -1,6 +1,8 @@ package define -import "github.com/containers/storage/pkg/idtools" +import ( + "github.com/containers/storage/pkg/idtools" +) // Info is the overall struct that describes the host system // running libpod/podman @@ -31,6 +33,7 @@ type HostInfo struct { CgroupControllers []string `json:"cgroupControllers"` Conmon *ConmonInfo `json:"conmon"` CPUs int `json:"cpus"` + CPUUtilization *CPUUsage `json:"cpuUtilization"` Distribution DistributionInfo `json:"distribution"` EventLogger string `json:"eventLogger"` Hostname string `json:"hostname"` @@ -108,11 +111,15 @@ type StoreInfo struct { GraphDriverName string `json:"graphDriverName"` GraphOptions map[string]interface{} `json:"graphOptions"` GraphRoot string `json:"graphRoot"` - GraphStatus map[string]string `json:"graphStatus"` - ImageCopyTmpDir string `json:"imageCopyTmpDir"` - ImageStore ImageStore `json:"imageStore"` - RunRoot string `json:"runRoot"` - VolumePath string `json:"volumePath"` + // GraphRootAllocated is how much space the graphroot has in bytes + GraphRootAllocated uint64 `json:"graphRootAllocated"` + // GraphRootUsed is how much of graphroot is used in bytes + GraphRootUsed uint64 `json:"graphRootUsed"` + GraphStatus map[string]string `json:"graphStatus"` + ImageCopyTmpDir string `json:"imageCopyTmpDir"` + ImageStore ImageStore `json:"imageStore"` + RunRoot string `json:"runRoot"` + VolumePath string `json:"volumePath"` } // ImageStore describes the image store. Right now only the number @@ -137,3 +144,9 @@ type Plugins struct { // FIXME what should we do with Authorization, docker seems to return nothing by default // Authorization []string `json:"authorization"` } + +type CPUUsage struct { + UserPercent float64 `json:"userPercent"` + SystemPercent float64 `json:"systemPercent"` + IdlePercent float64 `json:"idlePercent"` +} diff --git a/libpod/define/pod_inspect.go b/libpod/define/pod_inspect.go index e85a660a1..219ffade2 100644 --- a/libpod/define/pod_inspect.go +++ b/libpod/define/pod_inspect.go @@ -19,6 +19,8 @@ type InspectPodData struct { // CreateCommand is the full command plus arguments of the process the // container has been created with. CreateCommand []string `json:"CreateCommand,omitempty"` + // ExitPolicy of the pod. + ExitPolicy string `json:"ExitPolicy,omitempty"` // State represents the current state of the pod. State string `json:"State"` // Hostname is the hostname that the pod will set. diff --git a/libpod/events/config.go b/libpod/events/config.go index 35680a275..00cdca007 100644 --- a/libpod/events/config.go +++ b/libpod/events/config.go @@ -17,6 +17,8 @@ const ( Journald EventerType = iota // Null is a no-op events logger. It does not read or write events. Null EventerType = iota + // Memory indicates the event logger will hold events in memory + Memory EventerType = iota ) // Event describes the attributes of a libpod event @@ -55,7 +57,7 @@ type Details struct { // EventerOptions describe options that need to be passed to create // an eventer type EventerOptions struct { - // EventerType describes whether to use journald or a file + // EventerType describes whether to use journald, file or memory EventerType string // LogFilePath is the path to where the log file should reside if using // the file logger @@ -110,6 +112,8 @@ const ( System Type = "system" // Volume - event is related to volumes Volume Type = "volume" + // Machine - event is related to machine VM's + Machine Type = "machine" // Attach ... Attach Status = "attach" diff --git a/libpod/events/events.go b/libpod/events/events.go index 1745095fb..04417fd8d 100644 --- a/libpod/events/events.go +++ b/libpod/events/events.go @@ -20,6 +20,8 @@ func (et EventerType) String() string { return "file" case Journald: return "journald" + case Memory: + return "memory" case Null: return "none" default: @@ -34,6 +36,8 @@ func IsValidEventer(eventer string) bool { return true case Journald.String(): return true + case Memory.String(): + return true case Null.String(): return true default: @@ -41,7 +45,7 @@ func IsValidEventer(eventer string) bool { } } -// NewEvent creates a event struct and populates with +// NewEvent creates an event struct and populates with // the given status and time. func NewEvent(status Status) Event { return Event{ @@ -63,7 +67,7 @@ func (e *Event) ToJSONString() (string, error) { return string(b), err } -// ToHumanReadable returns human readable event as a formatted string +// ToHumanReadable returns human-readable event as a formatted string func (e *Event) ToHumanReadable(truncate bool) string { var humanFormat string id := e.ID @@ -90,7 +94,7 @@ func (e *Event) ToHumanReadable(truncate bool) string { } else { humanFormat = fmt.Sprintf("%s %s %s", e.Time, e.Type, e.Status) } - case Volume: + case Volume, Machine: humanFormat = fmt.Sprintf("%s %s %s %s", e.Time, e.Type, e.Status, e.Name) } return humanFormat @@ -99,19 +103,19 @@ func (e *Event) ToHumanReadable(truncate bool) string { // NewEventFromString takes stringified json and converts // it to an event func newEventFromJSONString(event string) (*Event, error) { - e := Event{} - if err := json.Unmarshal([]byte(event), &e); err != nil { + e := new(Event) + if err := json.Unmarshal([]byte(event), e); err != nil { return nil, err } - return &e, nil + return e, nil } -// ToString converts a Type to a string +// String converts a Type to a string func (t Type) String() string { return string(t) } -// ToString converts a status to a string +// String converts a status to a string func (s Status) String() string { return string(s) } @@ -123,6 +127,8 @@ func StringToType(name string) (Type, error) { return Container, nil case Image.String(): return Image, nil + case Machine.String(): + return Machine, nil case Network.String(): return Network, nil case Pod.String(): diff --git a/libpod/events/events_linux.go b/libpod/events/events_linux.go index 482d7d6dd..4320f2190 100644 --- a/libpod/events/events_linux.go +++ b/libpod/events/events_linux.go @@ -21,6 +21,8 @@ func NewEventer(options EventerOptions) (Eventer, error) { return EventLogFile{options}, nil case strings.ToUpper(Null.String()): return NewNullEventer(), nil + case strings.ToUpper(Memory.String()): + return NewMemoryEventer(), nil default: return nil, errors.Errorf("unknown event logger type: %s", strings.ToUpper(options.EventerType)) } diff --git a/libpod/events/memory.go b/libpod/events/memory.go new file mode 100644 index 000000000..b3e03d86b --- /dev/null +++ b/libpod/events/memory.go @@ -0,0 +1,49 @@ +package events + +import ( + "context" +) + +// EventMemory is the structure for event writing to a channel. It contains the eventer +// options and the event itself. Methods for reading and writing are also defined from it. +type EventMemory struct { + options EventerOptions + elements chan *Event +} + +// Write event to memory queue +func (e EventMemory) Write(event Event) (err error) { + e.elements <- &event + return +} + +// Read event(s) from memory queue +func (e EventMemory) Read(ctx context.Context, options ReadOptions) (err error) { + select { + case <-ctx.Done(): + return + default: + } + + select { + case event := <-e.elements: + options.EventChannel <- event + default: + } + return nil +} + +// String returns eventer type +func (e EventMemory) String() string { + return e.options.EventerType +} + +// NewMemoryEventer returns configured MemoryEventer +func NewMemoryEventer() Eventer { + return EventMemory{ + options: EventerOptions{ + EventerType: Memory.String(), + }, + elements: make(chan *Event, 100), + } +} diff --git a/libpod/info.go b/libpod/info.go index e0b490768..321680a81 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -5,11 +5,13 @@ import ( "bytes" "fmt" "io/ioutil" + "math" "os" "os/exec" "runtime" "strconv" "strings" + "syscall" "time" "github.com/containers/buildah" @@ -115,7 +117,10 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { if err != nil { return nil, errors.Wrapf(err, "error getting available cgroup controllers") } - + cpuUtil, err := getCPUUtilization() + if err != nil { + return nil, err + } info := define.HostInfo{ Arch: runtime.GOARCH, BuildahVersion: buildah.Version, @@ -123,6 +128,7 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { CgroupControllers: availableControllers, Linkmode: linkmode.Linkmode(), CPUs: runtime.NumCPU(), + CPUUtilization: cpuUtil, Distribution: hostDistributionInfo, LogDriver: r.config.Containers.LogDriver, EventLogger: r.eventer.String(), @@ -285,17 +291,25 @@ func (r *Runtime) storeInfo() (*define.StoreInfo, error) { } imageInfo := define.ImageStore{Number: len(images)} + var grStats syscall.Statfs_t + if err := syscall.Statfs(r.store.GraphRoot(), &grStats); err != nil { + return nil, errors.Wrapf(err, "unable to collect graph root usasge for %q", r.store.GraphRoot()) + } + allocated := uint64(grStats.Bsize) * grStats.Blocks info := define.StoreInfo{ - ImageStore: imageInfo, - ImageCopyTmpDir: os.Getenv("TMPDIR"), - ContainerStore: conInfo, - GraphRoot: r.store.GraphRoot(), - RunRoot: r.store.RunRoot(), - GraphDriverName: r.store.GraphDriverName(), - GraphOptions: nil, - VolumePath: r.config.Engine.VolumePath, - ConfigFile: configFile, + ImageStore: imageInfo, + ImageCopyTmpDir: os.Getenv("TMPDIR"), + ContainerStore: conInfo, + GraphRoot: r.store.GraphRoot(), + GraphRootAllocated: allocated, + GraphRootUsed: allocated - (uint64(grStats.Bsize) * grStats.Bfree), + RunRoot: r.store.RunRoot(), + GraphDriverName: r.store.GraphDriverName(), + GraphOptions: nil, + VolumePath: r.config.Engine.VolumePath, + ConfigFile: configFile, } + graphOptions := map[string]interface{}{} for _, o := range r.store.GraphOptions() { split := strings.SplitN(o, "=", 2) @@ -382,3 +396,44 @@ func (r *Runtime) GetHostDistributionInfo() define.DistributionInfo { } return dist } + +// getCPUUtilization Returns a CPUUsage object that summarizes CPU +// usage for userspace, system, and idle time. +func getCPUUtilization() (*define.CPUUsage, error) { + f, err := os.Open("/proc/stat") + if err != nil { + return nil, err + } + defer f.Close() + scanner := bufio.NewScanner(f) + // Read firt line of /proc/stat + for scanner.Scan() { + break + } + // column 1 is user, column 3 is system, column 4 is idle + stats := strings.Split(scanner.Text(), " ") + return statToPercent(stats) +} + +func statToPercent(stats []string) (*define.CPUUsage, error) { + // There is always an extra space between cpu and the first metric + userTotal, err := strconv.ParseFloat(stats[2], 64) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse user value %q", stats[1]) + } + systemTotal, err := strconv.ParseFloat(stats[4], 64) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse system value %q", stats[3]) + } + idleTotal, err := strconv.ParseFloat(stats[5], 64) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse idle value %q", stats[4]) + } + total := userTotal + systemTotal + idleTotal + s := define.CPUUsage{ + UserPercent: math.Round((userTotal/total*100)*100) / 100, + SystemPercent: math.Round((systemTotal/total*100)*100) / 100, + IdlePercent: math.Round((idleTotal/total*100)*100) / 100, + } + return &s, nil +} diff --git a/libpod/info_test.go b/libpod/info_test.go new file mode 100644 index 000000000..909b573c0 --- /dev/null +++ b/libpod/info_test.go @@ -0,0 +1,59 @@ +package libpod + +import ( + "fmt" + "testing" + + "github.com/containers/podman/v4/libpod/define" + "github.com/stretchr/testify/assert" +) + +func Test_statToPercent(t *testing.T) { + type args struct { + in0 []string + } + tests := []struct { + name string + args args + want *define.CPUUsage + wantErr assert.ErrorAssertionFunc + }{ + { + name: "GoodParse", + args: args{in0: []string{"cpu", " ", "33628064", "27537", "9696996", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, + want: &define.CPUUsage{ + UserPercent: 2.48, + SystemPercent: 0.71, + IdlePercent: 96.81, + }, + wantErr: assert.NoError, + }, + { + name: "BadUserValue", + args: args{in0: []string{"cpu", " ", "k", "27537", "9696996", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, + want: nil, + wantErr: assert.Error, + }, + { + name: "BadSystemValue", + args: args{in0: []string{"cpu", " ", "33628064", "27537", "k", "1314806705", "588142", "4775073", "2789228", "0", "598711", "0"}}, + want: nil, + wantErr: assert.Error, + }, + { + name: "BadIdleValue", + args: args{in0: []string{"cpu", " ", "33628064", "27537", "9696996", "k", "588142", "4775073", "2789228", "0", "598711", "0"}}, + want: nil, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := statToPercent(tt.args.in0) + if !tt.wantErr(t, err, fmt.Sprintf("statToPercent(%v)", tt.args.in0)) { + return + } + assert.Equalf(t, tt.want, got, "statToPercent(%v)", tt.args.in0) + }) + } +} diff --git a/libpod/kube.go b/libpod/kube.go index 8b75a0c44..5a5fe9d35 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -1034,7 +1034,11 @@ func generateKubeSecurityContext(c *Container) (*v1.SecurityContext, error) { if err != nil { return nil, errors.Wrapf(err, "failed to mount %s mountpoint", c.ID()) } - defer c.unmount(false) + defer func() { + if err := c.unmount(false); err != nil { + logrus.Errorf("Failed to unmount container: %v", err) + } + }() } logrus.Debugf("Looking in container for user: %s", c.User()) diff --git a/libpod/lock/file/file_lock_test.go b/libpod/lock/file/file_lock_test.go index 7ac8bf31a..2d7dded23 100644 --- a/libpod/lock/file/file_lock_test.go +++ b/libpod/lock/file/file_lock_test.go @@ -2,7 +2,6 @@ package file import ( "fmt" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -13,11 +12,9 @@ import ( // Test that creating and destroying locks work func TestCreateAndDeallocate(t *testing.T) { - d, err := ioutil.TempDir("", "filelock") - assert.NoError(t, err) - defer os.RemoveAll(d) + d := t.TempDir() - _, err = OpenFileLock(filepath.Join(d, "locks")) + _, err := OpenFileLock(filepath.Join(d, "locks")) assert.Error(t, err) l, err := CreateFileLock(filepath.Join(d, "locks")) @@ -47,9 +44,7 @@ func TestCreateAndDeallocate(t *testing.T) { // Test that creating and destroying locks work func TestLockAndUnlock(t *testing.T) { - d, err := ioutil.TempDir("", "filelock") - assert.NoError(t, err) - defer os.RemoveAll(d) + d := t.TempDir() l, err := CreateFileLock(filepath.Join(d, "locks")) assert.NoError(t, err) diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index 2770b040e..0c124cf0b 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -488,7 +488,7 @@ func (r *Runtime) GetRootlessNetNs(new bool) (*RootlessNetNS, error) { pid := strconv.Itoa(cmd.Process.Pid) err = ioutil.WriteFile(filepath.Join(rootlessNetNsDir, rootlessNetNsSilrp4netnsPidFile), []byte(pid), 0700) if err != nil { - errors.Wrap(err, "unable to write rootless-netns slirp4netns pid file") + return nil, errors.Wrap(err, "unable to write rootless-netns slirp4netns pid file") } defer func() { diff --git a/libpod/oci_attach_linux.go b/libpod/oci_attach_linux.go index b5eabec1f..06f8f8719 100644 --- a/libpod/oci_attach_linux.go +++ b/libpod/oci_attach_linux.go @@ -9,6 +9,7 @@ import ( "net" "os" "path/filepath" + "syscall" "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/libpod/define" @@ -259,7 +260,7 @@ func redirectResponseToOutputStreams(outputStream, errorStream io.Writer, writeO } } } - if er == io.EOF { + if errors.Is(er, io.EOF) || errors.Is(er, syscall.ECONNRESET) { break } if er != nil { @@ -274,11 +275,15 @@ func readStdio(conn *net.UnixConn, streams *define.AttachStreams, receiveStdoutE var err error select { case err = <-receiveStdoutError: - conn.CloseWrite() + if err := conn.CloseWrite(); err != nil { + logrus.Errorf("Failed to close stdin: %v", err) + } return err case err = <-stdinDone: if err == define.ErrDetach { - conn.CloseWrite() + if err := conn.CloseWrite(); err != nil { + logrus.Errorf("Failed to close stdin: %v", err) + } return err } if err == nil { diff --git a/libpod/options.go b/libpod/options.go index 98eb45e76..9b83cb76a 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1843,6 +1843,24 @@ func WithPodName(name string) PodCreateOption { } } +// WithPodExitPolicy sets the exit policy of the pod. +func WithPodExitPolicy(policy string) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + parsed, err := config.ParsePodExitPolicy(policy) + if err != nil { + return err + } + + pod.config.ExitPolicy = parsed + + return nil + } +} + // WithPodHostname sets the hostname of the pod. func WithPodHostname(hostname string) PodCreateOption { return func(pod *Pod) error { diff --git a/libpod/pod.go b/libpod/pod.go index 237c42901..2211d5be7 100644 --- a/libpod/pod.go +++ b/libpod/pod.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/libpod/lock" "github.com/opencontainers/runtime-spec/specs-go" @@ -70,6 +71,9 @@ type PodConfig struct { // container has been created with. CreateCommand []string `json:"CreateCommand,omitempty"` + // The pod's exit policy. + ExitPolicy config.PodExitPolicy `json:"ExitPolicy,omitempty"` + // ID of the pod's lock LockID uint32 `json:"lockID"` } diff --git a/libpod/pod_api.go b/libpod/pod_api.go index ba30d878e..73b28822b 100644 --- a/libpod/pod_api.go +++ b/libpod/pod_api.go @@ -2,6 +2,7 @@ package libpod import ( "context" + "fmt" "github.com/containers/common/pkg/cgroups" "github.com/containers/podman/v4/libpod/define" @@ -134,6 +135,10 @@ func (p *Pod) StopWithTimeout(ctx context.Context, cleanup bool, timeout int) (m p.lock.Lock() defer p.lock.Unlock() + return p.stopWithTimeout(ctx, cleanup, timeout) +} + +func (p *Pod) stopWithTimeout(ctx context.Context, cleanup bool, timeout int) (map[string]error, error) { if !p.valid { return nil, define.ErrPodRemoved } @@ -195,6 +200,51 @@ func (p *Pod) StopWithTimeout(ctx context.Context, cleanup bool, timeout int) (m return nil, nil } +// Stops the pod if only the infra containers remains running. +func (p *Pod) stopIfOnlyInfraRemains(ctx context.Context, ignoreID string) error { + p.lock.Lock() + defer p.lock.Unlock() + + infraID := "" + + if p.HasInfraContainer() { + infra, err := p.infraContainer() + if err != nil { + return err + } + infraID = infra.ID() + } + + allCtrs, err := p.runtime.state.PodContainers(p) + if err != nil { + return err + } + + for _, ctr := range allCtrs { + if ctr.ID() == infraID || ctr.ID() == ignoreID { + continue + } + + state, err := ctr.State() + if err != nil { + return fmt.Errorf("getting state of container %s: %w", ctr.ID(), err) + } + + switch state { + case define.ContainerStateExited, + define.ContainerStateRemoving, + define.ContainerStateStopping, + define.ContainerStateUnknown: + continue + default: + return nil + } + } + + _, err = p.stopWithTimeout(ctx, true, -1) + return err +} + // Cleanup cleans up all containers within a pod that have stopped. // All containers are cleaned up independently. An error with one container will // not prevent other containers being cleaned up. @@ -661,6 +711,7 @@ func (p *Pod) Inspect() (*define.InspectPodData, error) { Namespace: p.Namespace(), Created: p.CreatedTime(), CreateCommand: p.config.CreateCommand, + ExitPolicy: string(p.config.ExitPolicy), State: podState, Hostname: p.config.Hostname, Labels: p.Labels(), diff --git a/libpod/runtime.go b/libpod/runtime.go index 6c2323d88..f4cd9cf00 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -86,6 +86,10 @@ type Runtime struct { libimageEventsShutdown chan bool lockManager lock.Manager + // Worker + workerShutdown chan bool + workerChannel chan func() + // syslog describes whenever logrus should log to the syslog as well. // Note that the syslog hook will be enabled early in cmd/podman/syslog_linux.go // This bool is just needed so that we can set it for netavark interface. @@ -597,6 +601,8 @@ func makeRuntime(runtime *Runtime) (retErr error) { } } + runtime.startWorker() + // Mark the runtime as valid - ready to be used, cannot be modified // further runtime.valid = true @@ -817,6 +823,14 @@ func (r *Runtime) Shutdown(force bool) error { return define.ErrRuntimeStopped } + if r.workerShutdown != nil { + // Signal the worker routine to shutdown. The routine will + // process all pending work items and then read from the + // channel; we're blocked until all work items have been + // processed. + r.workerShutdown <- true + } + r.valid = false // Shutdown all containers if --force is given @@ -1144,7 +1158,7 @@ func (r *Runtime) getVolumePlugin(name string) (*plugin.VolumePlugin, error) { return plugin.GetVolumePlugin(name, pluginPath) } -// GetSecretsStoreageDir returns the directory that the secrets manager should take +// GetSecretsStorageDir returns the directory that the secrets manager should take func (r *Runtime) GetSecretsStorageDir() string { return filepath.Join(r.store.GraphRoot(), "secrets") } @@ -1192,7 +1206,17 @@ func (r *Runtime) Network() nettypes.ContainerNetwork { return r.network } -// Network returns the network interface which is used by the runtime +// GetDefaultNetworkName returns the network interface which is used by the runtime func (r *Runtime) GetDefaultNetworkName() string { return r.config.Network.DefaultNetwork } + +// RemoteURI returns the API server URI +func (r *Runtime) RemoteURI() string { + return r.config.Engine.RemoteURI +} + +// SetRemoteURI records the API server URI +func (r *Runtime) SetRemoteURI(uri string) { + r.config.Engine.RemoteURI = uri +} diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index fd3ffd199..df7174ac6 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -513,7 +513,9 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai case define.NoLogging, define.PassthroughLogging: break case define.JournaldLogging: - ctr.initializeJournal(ctx) + if err := ctr.initializeJournal(ctx); err != nil { + return nil, fmt.Errorf("failed to initialize journal: %w", err) + } default: if ctr.config.LogPath == "" { ctr.config.LogPath = filepath.Join(ctr.config.StaticDir, "ctr.log") diff --git a/libpod/runtime_pod_linux.go b/libpod/runtime_pod_linux.go index 2bbccfdf6..62ec7df60 100644 --- a/libpod/runtime_pod_linux.go +++ b/libpod/runtime_pod_linux.go @@ -199,10 +199,15 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool, // Go through and lock all containers so we can operate on them all at // once. // First loop also checks that we are ready to go ahead and remove. + containersLocked := true for _, ctr := range ctrs { ctrLock := ctr.lock ctrLock.Lock() - defer ctrLock.Unlock() + defer func() { + if containersLocked { + ctrLock.Unlock() + } + }() // If we're force-removing, no need to check status. if force { @@ -304,6 +309,12 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool, } } + // let's unlock the containers so if there is any cleanup process, it can terminate its execution + for _, ctr := range ctrs { + ctr.lock.Unlock() + } + containersLocked = false + // Remove pod cgroup, if present if p.state.CgroupPath != "" { logrus.Debugf("Removing pod cgroup %s", p.state.CgroupPath) @@ -332,7 +343,7 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool, } } if err == nil { - if err := conmonCgroup.Delete(); err != nil { + if err = conmonCgroup.Delete(); err != nil { if removalErr == nil { removalErr = errors.Wrapf(err, "error removing pod %s conmon cgroup", p.ID()) } else { diff --git a/libpod/runtime_worker.go b/libpod/runtime_worker.go new file mode 100644 index 000000000..ca44a27f7 --- /dev/null +++ b/libpod/runtime_worker.go @@ -0,0 +1,41 @@ +package libpod + +import ( + "time" +) + +func (r *Runtime) startWorker() { + if r.workerChannel == nil { + r.workerChannel = make(chan func(), 1) + r.workerShutdown = make(chan bool) + } + go func() { + for { + // Make sure to read all workers before + // checking if we're about to shutdown. + for len(r.workerChannel) > 0 { + w := <-r.workerChannel + w() + } + + select { + // We'll read from the shutdown channel only when all + // items above have been processed. + // + // (*Runtime).Shutdown() will block until until the + // item is read. + case <-r.workerShutdown: + return + + default: + time.Sleep(100 * time.Millisecond) + } + } + }() +} + +func (r *Runtime) queueWork(f func()) { + go func() { + r.workerChannel <- f + }() +} diff --git a/libpod/util.go b/libpod/util.go index 51fe60427..1753b4f34 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -55,8 +55,11 @@ func WaitForFile(path string, chWait chan error, timeout time.Duration) (bool, e if err := watcher.Add(filepath.Dir(path)); err == nil { inotifyEvents = watcher.Events } - defer watcher.Close() - defer watcher.Remove(filepath.Dir(path)) + defer func() { + if err := watcher.Close(); err != nil { + logrus.Errorf("Failed to close fsnotify watcher: %v", err) + } + }() } var timeoutChan <-chan time.Time |