diff options
Diffstat (limited to 'libpod')
51 files changed, 1279 insertions, 799 deletions
diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go index 4fd95a3cf..77c234892 100644 --- a/libpod/boltdb_state.go +++ b/libpod/boltdb_state.go @@ -109,6 +109,7 @@ func NewBoltState(path string, runtime *Runtime) (State, error) { runtimeConfigBkt, exitCodeBkt, exitCodeTimeStampBkt, + volCtrsBkt, } // Does the DB need an update? @@ -2551,6 +2552,11 @@ func (s *BoltState) AddVolume(volume *Volume) error { return err } + volCtrsBkt, err := getVolumeContainersBucket(tx) + if err != nil { + return err + } + // Check if we already have a volume with the given name volExists := allVolsBkt.Get(volName) if volExists != nil { @@ -2580,6 +2586,12 @@ func (s *BoltState) AddVolume(volume *Volume) error { } } + if volume.config.StorageID != "" { + if err := volCtrsBkt.Put([]byte(volume.config.StorageID), volName); err != nil { + return fmt.Errorf("storing volume %s container ID in DB: %w", volume.Name(), err) + } + } + if err := allVolsBkt.Put(volName, volName); err != nil { return fmt.Errorf("storing volume %s in all volumes bucket in DB: %w", volume.Name(), err) } @@ -2619,6 +2631,11 @@ func (s *BoltState) RemoveVolume(volume *Volume) error { return err } + volCtrIDBkt, err := getVolumeContainersBucket(tx) + if err != nil { + return err + } + // Check if the volume exists volDB := volBkt.Bucket(volName) if volDB == nil { @@ -2665,6 +2682,11 @@ func (s *BoltState) RemoveVolume(volume *Volume) error { if err := volBkt.DeleteBucket(volName); err != nil { return fmt.Errorf("removing volume %s from DB: %w", volume.Name(), err) } + if volume.config.StorageID != "" { + if err := volCtrIDBkt.Delete([]byte(volume.config.StorageID)); err != nil { + return fmt.Errorf("removing volume %s container ID from DB: %w", volume.Name(), err) + } + } return nil }) @@ -3618,3 +3640,34 @@ func (s *BoltState) AllPods() ([]*Pod, error) { return pods, nil } + +// ContainerIDIsVolume checks if the given c/storage container ID is used as +// backing storage for a volume. +func (s *BoltState) ContainerIDIsVolume(id string) (bool, error) { + if !s.valid { + return false, define.ErrDBClosed + } + + isVol := false + + db, err := s.getDBCon() + if err != nil { + return false, err + } + defer s.deferredCloseDBCon(db) + + err = db.View(func(tx *bolt.Tx) error { + volCtrsBkt, err := getVolumeContainersBucket(tx) + if err != nil { + return err + } + + volName := volCtrsBkt.Get([]byte(id)) + if volName != nil { + isVol = true + } + + return nil + }) + return isVol, err +} diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go index 87f1fa4eb..7f2d49b31 100644 --- a/libpod/boltdb_state_internal.go +++ b/libpod/boltdb_state_internal.go @@ -28,6 +28,7 @@ const ( execName = "exec" aliasesName = "aliases" runtimeConfigName = "runtime-config" + volumeCtrsName = "volume-ctrs" exitCodeName = "exit-code" exitCodeTimeStampName = "exit-code-time-stamp" @@ -67,6 +68,7 @@ var ( dependenciesBkt = []byte(dependenciesName) volDependenciesBkt = []byte(volCtrDependencies) networksBkt = []byte(networksName) + volCtrsBkt = []byte(volumeCtrsName) exitCodeBkt = []byte(exitCodeName) exitCodeTimeStampBkt = []byte(exitCodeTimeStampName) @@ -384,6 +386,14 @@ func getExitCodeTimeStampBucket(tx *bolt.Tx) (*bolt.Bucket, error) { return bkt, nil } +func getVolumeContainersBucket(tx *bolt.Tx) (*bolt.Bucket, error) { + bkt := tx.Bucket(volCtrsBkt) + if bkt == nil { + return nil, fmt.Errorf("volume containers bucket not found in DB: %w", define.ErrDBBadConfig) + } + return bkt, nil +} + func (s *BoltState) getContainerConfigFromDB(id []byte, config *ContainerConfig, ctrsBkt *bolt.Bucket) error { ctrBkt := ctrsBkt.Bucket(id) if ctrBkt == nil { @@ -528,6 +538,9 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu } } + // Need this for UsesVolumeDriver() so set it now. + volume.runtime = s.runtime + // Retrieve volume driver if volume.UsesVolumeDriver() { plugin, err := s.runtime.getVolumePlugin(volume.config) @@ -550,7 +563,6 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu } volume.lock = lock - volume.runtime = s.runtime volume.valid = true return nil diff --git a/libpod/container.go b/libpod/container.go index bdedafd22..a4eb33c49 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -3,7 +3,7 @@ package libpod import ( "bytes" "fmt" - "io/ioutil" + "io" "net" "os" "strings" @@ -351,12 +351,14 @@ func (c *Container) specFromState() (*spec.Spec, error) { if f, err := os.Open(c.state.ConfigPath); err == nil { returnSpec = new(spec.Spec) - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return nil, fmt.Errorf("reading container config: %w", err) } if err := json.Unmarshal(content, &returnSpec); err != nil { - return nil, fmt.Errorf("unmarshalling container config: %w", err) + // Malformed spec, just use c.config.Spec instead + logrus.Warnf("Error unmarshalling container %s config: %v", c.ID(), err) + return c.config.Spec, nil } } else if !os.IsNotExist(err) { // ignore when the file does not exist @@ -988,7 +990,7 @@ func (c *Container) cGroupPath() (string, error) { // the lookup. // See #10602 for more details. procPath := fmt.Sprintf("/proc/%d/cgroup", c.state.PID) - lines, err := ioutil.ReadFile(procPath) + lines, err := os.ReadFile(procPath) if err != nil { // If the file doesn't exist, it means the container could have been terminated // so report it. diff --git a/libpod/container_api.go b/libpod/container_api.go index dd47b4d12..be0ca0128 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "sync" @@ -479,7 +478,7 @@ func (c *Container) AddArtifact(name string, data []byte) error { return define.ErrCtrRemoved } - return ioutil.WriteFile(c.getArtifactPath(name), data, 0o740) + return os.WriteFile(c.getArtifactPath(name), data, 0o740) } // GetArtifact reads the specified artifact file from the container @@ -488,7 +487,7 @@ func (c *Container) GetArtifact(name string) ([]byte, error) { return nil, define.ErrCtrRemoved } - return ioutil.ReadFile(c.getArtifactPath(name)) + return os.ReadFile(c.getArtifactPath(name)) } // RemoveArtifact deletes the specified artifacts file diff --git a/libpod/container_copy_common.go b/libpod/container_copy_common.go new file mode 100644 index 000000000..d07b4c692 --- /dev/null +++ b/libpod/container_copy_common.go @@ -0,0 +1,213 @@ +//go:build linux || freebsd +// +build linux freebsd + +package libpod + +import ( + "errors" + "io" + "path/filepath" + "strings" + + buildahCopiah "github.com/containers/buildah/copier" + "github.com/containers/buildah/pkg/chrootuser" + "github.com/containers/buildah/util" + "github.com/containers/podman/v4/libpod/define" + "github.com/containers/podman/v4/pkg/rootless" + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" +) + +func (c *Container) copyFromArchive(path string, chown, noOverwriteDirNonDir bool, rename map[string]string, reader io.Reader) (func() error, error) { + var ( + mountPoint string + resolvedRoot string + resolvedPath string + unmount func() + err error + ) + + // Make sure that "/" copies the *contents* of the mount point and not + // the directory. + if path == "/" { + path = "/." + } + + // Optimization: only mount if the container is not already. + if c.state.Mounted { + mountPoint = c.state.Mountpoint + unmount = func() {} + } else { + // NOTE: make sure to unmount in error paths. + mountPoint, err = c.mount() + if err != nil { + return nil, err + } + unmount = func() { + if err := c.unmount(false); err != nil { + logrus.Errorf("Failed to unmount container: %v", err) + } + } + } + + resolvedRoot, resolvedPath, err = c.resolveCopyTarget(mountPoint, path) + if err != nil { + unmount() + return nil, err + } + + var idPair *idtools.IDPair + if chown { + // Make sure we chown the files to the container's main user and group ID. + user, err := getContainerUser(c, mountPoint) + if err != nil { + unmount() + return nil, err + } + idPair = &idtools.IDPair{UID: int(user.UID), GID: int(user.GID)} + } + + decompressed, err := archive.DecompressStream(reader) + if err != nil { + unmount() + return nil, err + } + + logrus.Debugf("Container copy *to* %q (resolved: %q) on container %q (ID: %s)", path, resolvedPath, c.Name(), c.ID()) + + return func() error { + defer unmount() + defer decompressed.Close() + putOptions := buildahCopiah.PutOptions{ + UIDMap: c.config.IDMappings.UIDMap, + GIDMap: c.config.IDMappings.GIDMap, + ChownDirs: idPair, + ChownFiles: idPair, + NoOverwriteDirNonDir: noOverwriteDirNonDir, + NoOverwriteNonDirDir: noOverwriteDirNonDir, + Rename: rename, + } + + return c.joinMountAndExec( + func() error { + return buildahCopiah.Put(resolvedRoot, resolvedPath, putOptions, decompressed) + }, + ) + }, nil +} + +func (c *Container) copyToArchive(path string, writer io.Writer) (func() error, error) { + var ( + mountPoint string + unmount func() + err error + ) + + // Optimization: only mount if the container is not already. + if c.state.Mounted { + mountPoint = c.state.Mountpoint + unmount = func() {} + } else { + // NOTE: make sure to unmount in error paths. + mountPoint, err = c.mount() + if err != nil { + return nil, err + } + 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) + if err != nil { + unmount() + return nil, err + } + + // We optimistically chown to the host user. In case of a hypothetical + // container-to-container copy, the reading side will chown back to the + // container user. + user, err := getContainerUser(c, mountPoint) + if err != nil { + unmount() + return nil, err + } + hostUID, hostGID, err := util.GetHostIDs( + idtoolsToRuntimeSpec(c.config.IDMappings.UIDMap), + idtoolsToRuntimeSpec(c.config.IDMappings.GIDMap), + user.UID, + user.GID, + ) + if err != nil { + unmount() + return nil, err + } + idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)} + + logrus.Debugf("Container copy *from* %q (resolved: %q) on container %q (ID: %s)", path, resolvedPath, c.Name(), c.ID()) + + return func() error { + defer unmount() + getOptions := buildahCopiah.GetOptions{ + // Unless the specified points to ".", we want to copy the base directory. + KeepDirectoryNames: statInfo.IsDir && filepath.Base(path) != ".", + UIDMap: c.config.IDMappings.UIDMap, + GIDMap: c.config.IDMappings.GIDMap, + ChownDirs: &idPair, + ChownFiles: &idPair, + Excludes: []string{"dev", "proc", "sys"}, + // Ignore EPERMs when copying from rootless containers + // since we cannot read TTY devices. Those are owned + // by the host's root and hence "nobody" inside the + // container's user namespace. + IgnoreUnreadable: rootless.IsRootless() && c.state.State == define.ContainerStateRunning, + } + return c.joinMountAndExec( + func() error { + return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedPath}, writer) + }, + ) + }, nil +} + +// getContainerUser returns the specs.User and ID mappings of the container. +func getContainerUser(container *Container, mountPoint string) (specs.User, error) { + userspec := container.config.User + + uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec) + u := specs.User{ + UID: uid, + GID: gid, + Username: userspec, + } + + if !strings.Contains(userspec, ":") { + groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) + if err2 != nil { + if !errors.Is(err2, chrootuser.ErrNoSuchUser) && err == nil { + err = err2 + } + } else { + u.AdditionalGids = groups + } + } + + return u, err +} + +// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec. +func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) { + for _, idmap := range idMaps { + tempIDMap := specs.LinuxIDMapping{ + ContainerID: uint32(idmap.ContainerID), + HostID: uint32(idmap.HostID), + Size: uint32(idmap.Size), + } + convertedIDMap = append(convertedIDMap, tempIDMap) + } + return convertedIDMap +} diff --git a/libpod/container_copy_freebsd.go b/libpod/container_copy_freebsd.go new file mode 100644 index 000000000..218f3917f --- /dev/null +++ b/libpod/container_copy_freebsd.go @@ -0,0 +1,13 @@ +package libpod + +// On FreeBSD, the container's mounts are in the global mount +// namespace so we can just execute the function directly. +func (c *Container) joinMountAndExec(f func() error) error { + return f() +} + +// Similarly, we can just use resolvePath for both running and stopped +// containers. +func (c *Container) resolveCopyTarget(mountPoint string, containerPath string) (string, string, error) { + return c.resolvePath(mountPoint, containerPath) +} diff --git a/libpod/container_copy_linux.go b/libpod/container_copy_linux.go index 557fead1e..3b029f08f 100644 --- a/libpod/container_copy_linux.go +++ b/libpod/container_copy_linux.go @@ -1,226 +1,14 @@ -//go:build linux -// +build linux - package libpod import ( - "errors" "fmt" - "io" "os" - "path/filepath" "runtime" - "strings" - buildahCopiah "github.com/containers/buildah/copier" - "github.com/containers/buildah/pkg/chrootuser" - "github.com/containers/buildah/util" "github.com/containers/podman/v4/libpod/define" - "github.com/containers/podman/v4/pkg/rootless" - "github.com/containers/storage/pkg/archive" - "github.com/containers/storage/pkg/idtools" - "github.com/opencontainers/runtime-spec/specs-go" - "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) -func (c *Container) copyFromArchive(path string, chown, noOverwriteDirNonDir bool, rename map[string]string, reader io.Reader) (func() error, error) { - var ( - mountPoint string - resolvedRoot string - resolvedPath string - unmount func() - err error - ) - - // Make sure that "/" copies the *contents* of the mount point and not - // the directory. - if path == "/" { - path = "/." - } - - // Optimization: only mount if the container is not already. - if c.state.Mounted { - mountPoint = c.state.Mountpoint - unmount = func() {} - } else { - // NOTE: make sure to unmount in error paths. - mountPoint, err = c.mount() - if err != nil { - return nil, err - } - unmount = func() { - if err := c.unmount(false); err != nil { - logrus.Errorf("Failed to unmount container: %v", err) - } - } - } - - if c.state.State == define.ContainerStateRunning { - resolvedRoot = "/" - resolvedPath = c.pathAbs(path) - } else { - resolvedRoot, resolvedPath, err = c.resolvePath(mountPoint, path) - if err != nil { - unmount() - return nil, err - } - } - - var idPair *idtools.IDPair - if chown { - // Make sure we chown the files to the container's main user and group ID. - user, err := getContainerUser(c, mountPoint) - if err != nil { - unmount() - return nil, err - } - idPair = &idtools.IDPair{UID: int(user.UID), GID: int(user.GID)} - } - - decompressed, err := archive.DecompressStream(reader) - if err != nil { - unmount() - return nil, err - } - - logrus.Debugf("Container copy *to* %q (resolved: %q) on container %q (ID: %s)", path, resolvedPath, c.Name(), c.ID()) - - return func() error { - defer unmount() - defer decompressed.Close() - putOptions := buildahCopiah.PutOptions{ - UIDMap: c.config.IDMappings.UIDMap, - GIDMap: c.config.IDMappings.GIDMap, - ChownDirs: idPair, - ChownFiles: idPair, - NoOverwriteDirNonDir: noOverwriteDirNonDir, - NoOverwriteNonDirDir: noOverwriteDirNonDir, - Rename: rename, - } - - return c.joinMountAndExec( - func() error { - return buildahCopiah.Put(resolvedRoot, resolvedPath, putOptions, decompressed) - }, - ) - }, nil -} - -func (c *Container) copyToArchive(path string, writer io.Writer) (func() error, error) { - var ( - mountPoint string - unmount func() - err error - ) - - // Optimization: only mount if the container is not already. - if c.state.Mounted { - mountPoint = c.state.Mountpoint - unmount = func() {} - } else { - // NOTE: make sure to unmount in error paths. - mountPoint, err = c.mount() - if err != nil { - return nil, err - } - 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) - if err != nil { - unmount() - return nil, err - } - - // We optimistically chown to the host user. In case of a hypothetical - // container-to-container copy, the reading side will chown back to the - // container user. - user, err := getContainerUser(c, mountPoint) - if err != nil { - unmount() - return nil, err - } - hostUID, hostGID, err := util.GetHostIDs( - idtoolsToRuntimeSpec(c.config.IDMappings.UIDMap), - idtoolsToRuntimeSpec(c.config.IDMappings.GIDMap), - user.UID, - user.GID, - ) - if err != nil { - unmount() - return nil, err - } - idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)} - - logrus.Debugf("Container copy *from* %q (resolved: %q) on container %q (ID: %s)", path, resolvedPath, c.Name(), c.ID()) - - return func() error { - defer unmount() - getOptions := buildahCopiah.GetOptions{ - // Unless the specified points to ".", we want to copy the base directory. - KeepDirectoryNames: statInfo.IsDir && filepath.Base(path) != ".", - UIDMap: c.config.IDMappings.UIDMap, - GIDMap: c.config.IDMappings.GIDMap, - ChownDirs: &idPair, - ChownFiles: &idPair, - Excludes: []string{"dev", "proc", "sys"}, - // Ignore EPERMs when copying from rootless containers - // since we cannot read TTY devices. Those are owned - // by the host's root and hence "nobody" inside the - // container's user namespace. - IgnoreUnreadable: rootless.IsRootless() && c.state.State == define.ContainerStateRunning, - } - return c.joinMountAndExec( - func() error { - return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedPath}, writer) - }, - ) - }, nil -} - -// getContainerUser returns the specs.User and ID mappings of the container. -func getContainerUser(container *Container, mountPoint string) (specs.User, error) { - userspec := container.config.User - - uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec) - u := specs.User{ - UID: uid, - GID: gid, - Username: userspec, - } - - if !strings.Contains(userspec, ":") { - groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) - if err2 != nil { - if !errors.Is(err2, chrootuser.ErrNoSuchUser) && err == nil { - err = err2 - } - } else { - u.AdditionalGids = groups - } - } - - return u, err -} - -// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec. -func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) { - for _, idmap := range idMaps { - tempIDMap := specs.LinuxIDMapping{ - ContainerID: uint32(idmap.ContainerID), - HostID: uint32(idmap.HostID), - Size: uint32(idmap.Size), - } - convertedIDMap = append(convertedIDMap, tempIDMap) - } - return convertedIDMap -} - // joinMountAndExec executes the specified function `f` inside the container's // mount and PID namespace. That allows for having the exact view on the // container's file system. @@ -288,3 +76,13 @@ func (c *Container) joinMountAndExec(f func() error) error { }() return <-errChan } + +func (c *Container) resolveCopyTarget(mountPoint string, containerPath string) (string, string, error) { + // If the container is running, we will execute the copy + // inside the container's mount namespace so we return a path + // relative to the container's root. + if c.state.State == define.ContainerStateRunning { + return "/", c.pathAbs(containerPath), nil + } + return c.resolvePath(mountPoint, containerPath) +} diff --git a/libpod/container_copy_unsupported.go b/libpod/container_copy_unsupported.go index 62937279a..703b0a74e 100644 --- a/libpod/container_copy_unsupported.go +++ b/libpod/container_copy_unsupported.go @@ -1,5 +1,5 @@ -//go:build !linux -// +build !linux +//go:build !linux && !freebsd +// +build !linux,!freebsd package libpod diff --git a/libpod/container_exec.go b/libpod/container_exec.go index 3a2cba52f..7896d1932 100644 --- a/libpod/container_exec.go +++ b/libpod/container_exec.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "io/ioutil" "net/http" "os" "path/filepath" @@ -928,7 +927,7 @@ func (c *Container) readExecExitCode(sessionID string) (int, error) { if err != nil { return -1, err } - ec, err := ioutil.ReadFile(exitFile) + ec, err := os.ReadFile(exitFile) if err != nil { return -1, err } diff --git a/libpod/container_graph.go b/libpod/container_graph.go index 96d61b756..d43579e4a 100644 --- a/libpod/container_graph.go +++ b/libpod/container_graph.go @@ -281,3 +281,94 @@ func startNode(ctx context.Context, node *containerNode, setError bool, ctrError startNode(ctx, successor, ctrErrored, ctrErrors, ctrsVisited, restart) } } + +// Visit a node on the container graph and remove it, or set an error if it +// failed to remove. Only intended for use in pod removal; do *not* use when +// removing individual containers. +// All containers are assumed to be *UNLOCKED* on running this function. +// Container locks will be acquired as necessary. +// Pod and infraID are optional. If a pod is given it must be *LOCKED*. +func removeNode(ctx context.Context, node *containerNode, pod *Pod, force bool, timeout *uint, setError bool, ctrErrors map[string]error, ctrsVisited map[string]bool, ctrNamedVolumes map[string]*ContainerNamedVolume) { + // If we already visited this node, we're done. + if ctrsVisited[node.id] { + return + } + + // Someone who depends on us failed. + // Mark us as failed and recurse. + if setError { + ctrsVisited[node.id] = true + ctrErrors[node.id] = fmt.Errorf("a container that depends on container %s could not be removed: %w", node.id, define.ErrCtrStateInvalid) + + // Hit anyone who depends on us, set errors there as well. + for _, successor := range node.dependsOn { + removeNode(ctx, successor, pod, force, timeout, true, ctrErrors, ctrsVisited, ctrNamedVolumes) + } + } + + // Does anyone still depend on us? + // Cannot remove if true. Once all our dependencies have been removed, + // we will be removed. + for _, dep := range node.dependedOn { + // The container that depends on us hasn't been removed yet. + // OK to continue on + if ok := ctrsVisited[dep.id]; !ok { + return + } + } + + // Going to try to remove the node, mark us as visited + ctrsVisited[node.id] = true + + ctrErrored := false + + // Verify that all that depend on us are gone. + // Graph traversal should guarantee this is true, but this isn't that + // expensive, and it's better to be safe. + for _, dep := range node.dependedOn { + if _, err := node.container.runtime.GetContainer(dep.id); err == nil { + ctrErrored = true + ctrErrors[node.id] = fmt.Errorf("a container that depends on container %s still exists: %w", node.id, define.ErrDepExists) + } + } + + // Lock the container + node.container.lock.Lock() + + // Gate all subsequent bits behind a ctrErrored check - we don't want to + // proceed if a previous step failed. + if !ctrErrored { + if err := node.container.syncContainer(); err != nil { + ctrErrored = true + ctrErrors[node.id] = err + } + } + + if !ctrErrored { + for _, vol := range node.container.config.NamedVolumes { + ctrNamedVolumes[vol.Name] = vol + } + + if pod != nil && pod.state.InfraContainerID == node.id { + pod.state.InfraContainerID = "" + if err := pod.save(); err != nil { + ctrErrored = true + ctrErrors[node.id] = fmt.Errorf("error removing infra container %s from pod %s: %w", node.id, pod.ID(), err) + } + } + } + + if !ctrErrored { + if err := node.container.runtime.removeContainer(ctx, node.container, force, false, true, false, timeout); err != nil { + ctrErrored = true + ctrErrors[node.id] = err + } + } + + node.container.lock.Unlock() + + // Recurse to anyone who we depend on and remove them + for _, successor := range node.dependsOn { + removeNode(ctx, successor, pod, force, timeout, ctrErrored, ctrErrors, ctrsVisited, ctrNamedVolumes) + } +} diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index b72d843b6..e4089efa6 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -3,20 +3,15 @@ package libpod import ( "errors" "fmt" - "sort" "strings" - "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/libpod/driver" "github.com/containers/podman/v4/pkg/util" "github.com/containers/storage/types" units "github.com/docker/go-units" spec "github.com/opencontainers/runtime-spec/specs-go" - "github.com/opencontainers/runtime-tools/generate" - "github.com/opencontainers/runtime-tools/validate" "github.com/sirupsen/logrus" - "github.com/syndtr/gocapability/capability" ) // inspectLocked inspects a container for low-level information. @@ -163,8 +158,6 @@ func (c *Container) getContainerInspectData(size bool, driverData *define.Driver Driver: driverData.Name, MountLabel: config.MountLabel, ProcessLabel: config.ProcessLabel, - EffectiveCaps: ctrSpec.Process.Capabilities.Effective, - BoundingCaps: ctrSpec.Process.Capabilities.Bounding, AppArmorProfile: ctrSpec.Process.ApparmorProfile, ExecIDs: execIDs, GraphDriver: driverData, @@ -173,6 +166,10 @@ func (c *Container) getContainerInspectData(size bool, driverData *define.Driver IsInfra: c.IsInfra(), IsService: c.IsService(), } + if ctrSpec.Process.Capabilities != nil { + data.EffectiveCaps = ctrSpec.Process.Capabilities.Effective + data.BoundingCaps = ctrSpec.Process.Capabilities.Bounding + } if c.state.ConfigPath != "" { data.OCIConfigPath = c.state.ConfigPath @@ -484,11 +481,6 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named hostConfig.ShmSize = c.config.ShmSize hostConfig.Runtime = "oci" - // This is very expensive to initialize. - // So we don't want to initialize it unless we absolutely have to - IE, - // there are things that require a major:minor to path translation. - var deviceNodes map[string]string - // Annotations if ctrSpec.Annotations != nil { hostConfig.ContainerIDFile = ctrSpec.Annotations[define.InspectAnnotationCIDFile] @@ -506,109 +498,8 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named } } - // Resource limits - if ctrSpec.Linux != nil { - if ctrSpec.Linux.Resources != nil { - if ctrSpec.Linux.Resources.CPU != nil { - if ctrSpec.Linux.Resources.CPU.Shares != nil { - hostConfig.CpuShares = *ctrSpec.Linux.Resources.CPU.Shares - } - if ctrSpec.Linux.Resources.CPU.Period != nil { - hostConfig.CpuPeriod = *ctrSpec.Linux.Resources.CPU.Period - } - if ctrSpec.Linux.Resources.CPU.Quota != nil { - hostConfig.CpuQuota = *ctrSpec.Linux.Resources.CPU.Quota - } - if ctrSpec.Linux.Resources.CPU.RealtimePeriod != nil { - hostConfig.CpuRealtimePeriod = *ctrSpec.Linux.Resources.CPU.RealtimePeriod - } - if ctrSpec.Linux.Resources.CPU.RealtimeRuntime != nil { - hostConfig.CpuRealtimeRuntime = *ctrSpec.Linux.Resources.CPU.RealtimeRuntime - } - hostConfig.CpusetCpus = ctrSpec.Linux.Resources.CPU.Cpus - hostConfig.CpusetMems = ctrSpec.Linux.Resources.CPU.Mems - } - if ctrSpec.Linux.Resources.Memory != nil { - if ctrSpec.Linux.Resources.Memory.Limit != nil { - hostConfig.Memory = *ctrSpec.Linux.Resources.Memory.Limit - } - if ctrSpec.Linux.Resources.Memory.Reservation != nil { - hostConfig.MemoryReservation = *ctrSpec.Linux.Resources.Memory.Reservation - } - if ctrSpec.Linux.Resources.Memory.Swap != nil { - hostConfig.MemorySwap = *ctrSpec.Linux.Resources.Memory.Swap - } - if ctrSpec.Linux.Resources.Memory.Swappiness != nil { - hostConfig.MemorySwappiness = int64(*ctrSpec.Linux.Resources.Memory.Swappiness) - } else { - // Swappiness has a default of -1 - hostConfig.MemorySwappiness = -1 - } - if ctrSpec.Linux.Resources.Memory.DisableOOMKiller != nil { - hostConfig.OomKillDisable = *ctrSpec.Linux.Resources.Memory.DisableOOMKiller - } - } - if ctrSpec.Linux.Resources.Pids != nil { - hostConfig.PidsLimit = ctrSpec.Linux.Resources.Pids.Limit - } - hostConfig.CgroupConf = ctrSpec.Linux.Resources.Unified - if ctrSpec.Linux.Resources.BlockIO != nil { - if ctrSpec.Linux.Resources.BlockIO.Weight != nil { - hostConfig.BlkioWeight = *ctrSpec.Linux.Resources.BlockIO.Weight - } - hostConfig.BlkioWeightDevice = []define.InspectBlkioWeightDevice{} - for _, dev := range ctrSpec.Linux.Resources.BlockIO.WeightDevice { - key := fmt.Sprintf("%d:%d", dev.Major, dev.Minor) - // TODO: how do we handle LeafWeight vs - // Weight? For now, ignore anything - // without Weight set. - if dev.Weight == nil { - logrus.Infof("Ignoring weight device %s as it lacks a weight", key) - continue - } - if deviceNodes == nil { - nodes, err := util.FindDeviceNodes() - if err != nil { - return nil, err - } - deviceNodes = nodes - } - path, ok := deviceNodes[key] - if !ok { - logrus.Infof("Could not locate weight device %s in system devices", key) - continue - } - weightDev := define.InspectBlkioWeightDevice{} - weightDev.Path = path - weightDev.Weight = *dev.Weight - hostConfig.BlkioWeightDevice = append(hostConfig.BlkioWeightDevice, weightDev) - } - - readBps, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleReadBpsDevice) - if err != nil { - return nil, err - } - hostConfig.BlkioDeviceReadBps = readBps - - writeBps, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleWriteBpsDevice) - if err != nil { - return nil, err - } - hostConfig.BlkioDeviceWriteBps = writeBps - - readIops, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleReadIOPSDevice) - if err != nil { - return nil, err - } - hostConfig.BlkioDeviceReadIOps = readIops - - writeIops, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleWriteIOPSDevice) - if err != nil { - return nil, err - } - hostConfig.BlkioDeviceWriteIOps = writeIops - } - } + if err := c.platformInspectContainerHostConfig(ctrSpec, hostConfig); err != nil { + return nil, err } // NanoCPUs. @@ -659,182 +550,6 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named hostConfig.PortBindings = make(map[string][]define.InspectHostPort) } - // Cap add and cap drop. - // We need a default set of capabilities to compare against. - // The OCI generate package has one, and is commonly used, so we'll - // use it. - // Problem: there are 5 sets of capabilities. - // Use the bounding set for this computation, it's the most encompassing - // (but still not perfect). - capAdd := []string{} - capDrop := []string{} - // No point in continuing if we got a spec without a Process block... - if ctrSpec.Process != nil { - // Max an O(1) lookup table for default bounding caps. - boundingCaps := make(map[string]bool) - g, err := generate.New("linux") - if err != nil { - return nil, err - } - if !hostConfig.Privileged { - for _, cap := range g.Config.Process.Capabilities.Bounding { - boundingCaps[cap] = true - } - } else { - // If we are privileged, use all caps. - for _, cap := range capability.List() { - if g.HostSpecific && cap > validate.LastCap() { - continue - } - boundingCaps[fmt.Sprintf("CAP_%s", strings.ToUpper(cap.String()))] = true - } - } - // Iterate through spec caps. - // If it's not in default bounding caps, it was added. - // If it is, delete from the default set. Whatever remains after - // we finish are the dropped caps. - for _, cap := range ctrSpec.Process.Capabilities.Bounding { - if _, ok := boundingCaps[cap]; ok { - delete(boundingCaps, cap) - } else { - capAdd = append(capAdd, cap) - } - } - for cap := range boundingCaps { - capDrop = append(capDrop, cap) - } - // Sort CapDrop so it displays in consistent order (GH #9490) - sort.Strings(capDrop) - } - hostConfig.CapAdd = capAdd - hostConfig.CapDrop = capDrop - 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 != "" { - hostConfig.IpcMode = fmt.Sprintf("ns:%s", ns.Path) - } else { - break - } - } - } - case c.config.NoShm: - hostConfig.IpcMode = "none" - case c.config.NoShmShare: - hostConfig.IpcMode = "private" - } - if hostConfig.IpcMode == "" { - hostConfig.IpcMode = "shareable" - } - - // Cgroup namespace mode - cgroupMode := "" - if c.config.CgroupNsCtr != "" { - cgroupMode = fmt.Sprintf("container:%s", c.config.CgroupNsCtr) - } else if ctrSpec.Linux != nil { - // Locate the spec's cgroup namespace - // If there is none, it's cgroup=host. - // If there is one and it has a path, it's "ns:". - // If there is no path, it's private. - for _, ns := range ctrSpec.Linux.Namespaces { - if ns.Type == spec.CgroupNamespace { - if ns.Path != "" { - cgroupMode = fmt.Sprintf("ns:%s", ns.Path) - } else { - cgroupMode = "private" - } - } - } - if cgroupMode == "" { - cgroupMode = "host" - } - } - hostConfig.CgroupMode = cgroupMode - - // Cgroup parent - // Need to check if it's the default, and not print if so. - defaultCgroupParent := "" - switch c.CgroupManager() { - case config.CgroupfsCgroupsManager: - defaultCgroupParent = CgroupfsDefaultCgroupParent - case config.SystemdCgroupsManager: - defaultCgroupParent = SystemdDefaultCgroupParent - } - if c.config.CgroupParent != defaultCgroupParent { - hostConfig.CgroupParent = c.config.CgroupParent - } - hostConfig.CgroupManager = c.CgroupManager() - - // PID namespace mode - pidMode := "" - if c.config.PIDNsCtr != "" { - pidMode = fmt.Sprintf("container:%s", c.config.PIDNsCtr) - } else if ctrSpec.Linux != nil { - // Locate the spec's PID namespace. - // If there is none, it's pid=host. - // If there is one and it has a path, it's "ns:". - // If there is no path, it's default - the empty string. - for _, ns := range ctrSpec.Linux.Namespaces { - if ns.Type == spec.PIDNamespace { - if ns.Path != "" { - pidMode = fmt.Sprintf("ns:%s", ns.Path) - } else { - pidMode = "private" - } - break - } - } - if pidMode == "" { - pidMode = "host" - } - } - hostConfig.PidMode = pidMode - - // UTS namespace mode - utsMode := c.NamespaceMode(spec.UTSNamespace, ctrSpec) - - hostConfig.UTSMode = utsMode - - // User namespace mode - usernsMode := "" - if c.config.UserNsCtr != "" { - usernsMode = fmt.Sprintf("container:%s", c.config.UserNsCtr) - } else if ctrSpec.Linux != nil { - // Locate the spec's user namespace. - // If there is none, it's default - the empty string. - // If there is one, it's "private" if no path, or "ns:" if - // there's a path. - - for _, ns := range ctrSpec.Linux.Namespaces { - if ns.Type == spec.UserNamespace { - if ns.Path != "" { - usernsMode = fmt.Sprintf("ns:%s", ns.Path) - } else { - usernsMode = "private" - } - } - } - } - hostConfig.UsernsMode = usernsMode - if c.config.IDMappings.UIDMap != nil && c.config.IDMappings.GIDMap != nil { - hostConfig.IDMappings = generateIDMappings(c.config.IDMappings) - } - // Devices - // Do not include if privileged - assumed that all devices will be - // included. - var err error - hostConfig.Devices, err = c.GetDevices(hostConfig.Privileged, *ctrSpec, deviceNodes) - if err != nil { - return nil, err - } - // Ulimits hostConfig.Ulimits = []define.InspectUlimit{} if ctrSpec.Process != nil { diff --git a/libpod/container_inspect_freebsd.go b/libpod/container_inspect_freebsd.go new file mode 100644 index 000000000..8b4e8df87 --- /dev/null +++ b/libpod/container_inspect_freebsd.go @@ -0,0 +1,17 @@ +package libpod + +import ( + "github.com/containers/podman/v4/libpod/define" + spec "github.com/opencontainers/runtime-spec/specs-go" +) + +func (c *Container) platformInspectContainerHostConfig(ctrSpec *spec.Spec, hostConfig *define.InspectContainerHostConfig) error { + // Not sure what to put here. FreeBSD jails use pids from the + // global pool but can only see their own pids. + hostConfig.PidMode = "host" + + // UTS namespace mode + hostConfig.UTSMode = c.NamespaceMode(spec.UTSNamespace, ctrSpec) + + return nil +} diff --git a/libpod/container_inspect_linux.go b/libpod/container_inspect_linux.go new file mode 100644 index 000000000..355690d70 --- /dev/null +++ b/libpod/container_inspect_linux.go @@ -0,0 +1,306 @@ +package libpod + +import ( + "fmt" + "sort" + "strings" + + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v4/libpod/define" + "github.com/containers/podman/v4/pkg/util" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" + "github.com/opencontainers/runtime-tools/validate" + "github.com/sirupsen/logrus" + "github.com/syndtr/gocapability/capability" +) + +func (c *Container) platformInspectContainerHostConfig(ctrSpec *spec.Spec, hostConfig *define.InspectContainerHostConfig) error { + // This is very expensive to initialize. + // So we don't want to initialize it unless we absolutely have to - IE, + // there are things that require a major:minor to path translation. + var deviceNodes map[string]string + + // Resource limits + if ctrSpec.Linux != nil { + if ctrSpec.Linux.Resources != nil { + if ctrSpec.Linux.Resources.CPU != nil { + if ctrSpec.Linux.Resources.CPU.Shares != nil { + hostConfig.CpuShares = *ctrSpec.Linux.Resources.CPU.Shares + } + if ctrSpec.Linux.Resources.CPU.Period != nil { + hostConfig.CpuPeriod = *ctrSpec.Linux.Resources.CPU.Period + } + if ctrSpec.Linux.Resources.CPU.Quota != nil { + hostConfig.CpuQuota = *ctrSpec.Linux.Resources.CPU.Quota + } + if ctrSpec.Linux.Resources.CPU.RealtimePeriod != nil { + hostConfig.CpuRealtimePeriod = *ctrSpec.Linux.Resources.CPU.RealtimePeriod + } + if ctrSpec.Linux.Resources.CPU.RealtimeRuntime != nil { + hostConfig.CpuRealtimeRuntime = *ctrSpec.Linux.Resources.CPU.RealtimeRuntime + } + hostConfig.CpusetCpus = ctrSpec.Linux.Resources.CPU.Cpus + hostConfig.CpusetMems = ctrSpec.Linux.Resources.CPU.Mems + } + if ctrSpec.Linux.Resources.Memory != nil { + if ctrSpec.Linux.Resources.Memory.Limit != nil { + hostConfig.Memory = *ctrSpec.Linux.Resources.Memory.Limit + } + if ctrSpec.Linux.Resources.Memory.Reservation != nil { + hostConfig.MemoryReservation = *ctrSpec.Linux.Resources.Memory.Reservation + } + if ctrSpec.Linux.Resources.Memory.Swap != nil { + hostConfig.MemorySwap = *ctrSpec.Linux.Resources.Memory.Swap + } + if ctrSpec.Linux.Resources.Memory.Swappiness != nil { + hostConfig.MemorySwappiness = int64(*ctrSpec.Linux.Resources.Memory.Swappiness) + } else { + // Swappiness has a default of -1 + hostConfig.MemorySwappiness = -1 + } + if ctrSpec.Linux.Resources.Memory.DisableOOMKiller != nil { + hostConfig.OomKillDisable = *ctrSpec.Linux.Resources.Memory.DisableOOMKiller + } + } + if ctrSpec.Linux.Resources.Pids != nil { + hostConfig.PidsLimit = ctrSpec.Linux.Resources.Pids.Limit + } + hostConfig.CgroupConf = ctrSpec.Linux.Resources.Unified + if ctrSpec.Linux.Resources.BlockIO != nil { + if ctrSpec.Linux.Resources.BlockIO.Weight != nil { + hostConfig.BlkioWeight = *ctrSpec.Linux.Resources.BlockIO.Weight + } + hostConfig.BlkioWeightDevice = []define.InspectBlkioWeightDevice{} + for _, dev := range ctrSpec.Linux.Resources.BlockIO.WeightDevice { + key := fmt.Sprintf("%d:%d", dev.Major, dev.Minor) + // TODO: how do we handle LeafWeight vs + // Weight? For now, ignore anything + // without Weight set. + if dev.Weight == nil { + logrus.Infof("Ignoring weight device %s as it lacks a weight", key) + continue + } + if deviceNodes == nil { + nodes, err := util.FindDeviceNodes() + if err != nil { + return err + } + deviceNodes = nodes + } + path, ok := deviceNodes[key] + if !ok { + logrus.Infof("Could not locate weight device %s in system devices", key) + continue + } + weightDev := define.InspectBlkioWeightDevice{} + weightDev.Path = path + weightDev.Weight = *dev.Weight + hostConfig.BlkioWeightDevice = append(hostConfig.BlkioWeightDevice, weightDev) + } + + readBps, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleReadBpsDevice) + if err != nil { + return err + } + hostConfig.BlkioDeviceReadBps = readBps + + writeBps, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleWriteBpsDevice) + if err != nil { + return err + } + hostConfig.BlkioDeviceWriteBps = writeBps + + readIops, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleReadIOPSDevice) + if err != nil { + return err + } + hostConfig.BlkioDeviceReadIOps = readIops + + writeIops, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleWriteIOPSDevice) + if err != nil { + return err + } + hostConfig.BlkioDeviceWriteIOps = writeIops + } + } + } + + // Cap add and cap drop. + // We need a default set of capabilities to compare against. + // The OCI generate package has one, and is commonly used, so we'll + // use it. + // Problem: there are 5 sets of capabilities. + // Use the bounding set for this computation, it's the most encompassing + // (but still not perfect). + capAdd := []string{} + capDrop := []string{} + // No point in continuing if we got a spec without a Process block... + if ctrSpec.Process != nil { + // Max an O(1) lookup table for default bounding caps. + boundingCaps := make(map[string]bool) + g, err := generate.New("linux") + if err != nil { + return err + } + if !hostConfig.Privileged { + for _, cap := range g.Config.Process.Capabilities.Bounding { + boundingCaps[cap] = true + } + } else { + // If we are privileged, use all caps. + for _, cap := range capability.List() { + if g.HostSpecific && cap > validate.LastCap() { + continue + } + boundingCaps[fmt.Sprintf("CAP_%s", strings.ToUpper(cap.String()))] = true + } + } + // Iterate through spec caps. + // If it's not in default bounding caps, it was added. + // If it is, delete from the default set. Whatever remains after + // we finish are the dropped caps. + for _, cap := range ctrSpec.Process.Capabilities.Bounding { + if _, ok := boundingCaps[cap]; ok { + delete(boundingCaps, cap) + } else { + capAdd = append(capAdd, cap) + } + } + for cap := range boundingCaps { + capDrop = append(capDrop, cap) + } + // Sort CapDrop so it displays in consistent order (GH #9490) + sort.Strings(capDrop) + } + hostConfig.CapAdd = capAdd + hostConfig.CapDrop = capDrop + 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 != "" { + hostConfig.IpcMode = fmt.Sprintf("ns:%s", ns.Path) + } else { + break + } + } + } + case c.config.NoShm: + hostConfig.IpcMode = "none" + case c.config.NoShmShare: + hostConfig.IpcMode = "private" + } + if hostConfig.IpcMode == "" { + hostConfig.IpcMode = "shareable" + } + + // Cgroup namespace mode + cgroupMode := "" + if c.config.CgroupNsCtr != "" { + cgroupMode = fmt.Sprintf("container:%s", c.config.CgroupNsCtr) + } else if ctrSpec.Linux != nil { + // Locate the spec's cgroup namespace + // If there is none, it's cgroup=host. + // If there is one and it has a path, it's "ns:". + // If there is no path, it's private. + for _, ns := range ctrSpec.Linux.Namespaces { + if ns.Type == spec.CgroupNamespace { + if ns.Path != "" { + cgroupMode = fmt.Sprintf("ns:%s", ns.Path) + } else { + cgroupMode = "private" + } + } + } + if cgroupMode == "" { + cgroupMode = "host" + } + } + hostConfig.CgroupMode = cgroupMode + + // Cgroup parent + // Need to check if it's the default, and not print if so. + defaultCgroupParent := "" + switch c.CgroupManager() { + case config.CgroupfsCgroupsManager: + defaultCgroupParent = CgroupfsDefaultCgroupParent + case config.SystemdCgroupsManager: + defaultCgroupParent = SystemdDefaultCgroupParent + } + if c.config.CgroupParent != defaultCgroupParent { + hostConfig.CgroupParent = c.config.CgroupParent + } + hostConfig.CgroupManager = c.CgroupManager() + + // PID namespace mode + pidMode := "" + if c.config.PIDNsCtr != "" { + pidMode = fmt.Sprintf("container:%s", c.config.PIDNsCtr) + } else if ctrSpec.Linux != nil { + // Locate the spec's PID namespace. + // If there is none, it's pid=host. + // If there is one and it has a path, it's "ns:". + // If there is no path, it's default - the empty string. + for _, ns := range ctrSpec.Linux.Namespaces { + if ns.Type == spec.PIDNamespace { + if ns.Path != "" { + pidMode = fmt.Sprintf("ns:%s", ns.Path) + } else { + pidMode = "private" + } + break + } + } + if pidMode == "" { + pidMode = "host" + } + } + hostConfig.PidMode = pidMode + + // UTS namespace mode + utsMode := c.NamespaceMode(spec.UTSNamespace, ctrSpec) + + hostConfig.UTSMode = utsMode + + // User namespace mode + usernsMode := "" + if c.config.UserNsCtr != "" { + usernsMode = fmt.Sprintf("container:%s", c.config.UserNsCtr) + } else if ctrSpec.Linux != nil { + // Locate the spec's user namespace. + // If there is none, it's default - the empty string. + // If there is one, it's "private" if no path, or "ns:" if + // there's a path. + + for _, ns := range ctrSpec.Linux.Namespaces { + if ns.Type == spec.UserNamespace { + if ns.Path != "" { + usernsMode = fmt.Sprintf("ns:%s", ns.Path) + } else { + usernsMode = "private" + } + } + } + } + hostConfig.UsernsMode = usernsMode + if c.config.IDMappings.UIDMap != nil && c.config.IDMappings.GIDMap != nil { + hostConfig.IDMappings = generateIDMappings(c.config.IDMappings) + } + // Devices + // Do not include if privileged - assumed that all devices will be + // included. + var err error + hostConfig.Devices, err = c.GetDevices(hostConfig.Privileged, *ctrSpec, deviceNodes) + if err != nil { + return err + } + + return nil +} diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 994243805..9bf93412d 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strconv" @@ -201,7 +200,7 @@ func (c *Container) waitForExitFileAndSync() error { // This assumes the exit file already exists. func (c *Container) handleExitFile(exitFile string, fi os.FileInfo) error { c.state.FinishedTime = ctime.Created(fi) - statusCodeStr, err := ioutil.ReadFile(exitFile) + statusCodeStr, err := os.ReadFile(exitFile) if err != nil { return fmt.Errorf("failed to read exit file for container %s: %w", c.ID(), err) } @@ -2089,7 +2088,7 @@ func (c *Container) saveSpec(spec *spec.Spec) error { if err != nil { return fmt.Errorf("exporting runtime spec for container %s to JSON: %w", c.ID(), err) } - if err := ioutil.WriteFile(jsonPath, fileJSON, 0644); err != nil { + if err := os.WriteFile(jsonPath, fileJSON, 0644); err != nil { return fmt.Errorf("writing runtime spec JSON for container %s to disk: %w", c.ID(), err) } @@ -2343,7 +2342,7 @@ func (c *Container) extractSecretToCtrStorage(secr *ContainerSecret) error { if err != nil { return fmt.Errorf("unable to extract secret: %w", err) } - err = ioutil.WriteFile(secretFile, data, 0644) + err = os.WriteFile(secretFile, data, 0644) if err != nil { return fmt.Errorf("unable to create %s: %w", secretFile, err) } diff --git a/libpod/container_internal_common.go b/libpod/container_internal_common.go index c7f59aba5..29107d4b6 100644 --- a/libpod/container_internal_common.go +++ b/libpod/container_internal_common.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "math" "os" "os/user" @@ -110,7 +109,11 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { // If the flag to mount all devices is set for a privileged container, add // all the devices from the host's machine into the container if c.config.MountAllDevices { - if err := util.AddPrivilegedDevices(&g); err != nil { + systemdMode := false + if c.config.Systemd != nil { + systemdMode = *c.config.Systemd + } + if err := util.AddPrivilegedDevices(&g, systemdMode); err != nil { return nil, err } } @@ -531,7 +534,7 @@ func (c *Container) isWorkDirSymlink(resolvedPath string) bool { } if resolvedSymlink != "" { _, resolvedSymlinkWorkdir, err := c.resolvePath(c.state.Mountpoint, resolvedSymlink) - if isPathOnVolume(c, resolvedSymlinkWorkdir) || isPathOnBindMount(c, resolvedSymlinkWorkdir) { + if isPathOnVolume(c, resolvedSymlinkWorkdir) || isPathOnMount(c, resolvedSymlinkWorkdir) { // Resolved symlink exists on external volume or mount return true } @@ -564,7 +567,7 @@ func (c *Container) resolveWorkDir() error { // If the specified workdir is a subdir of a volume or mount, // we don't need to do anything. The runtime is taking care of // that. - if isPathOnVolume(c, workdir) || isPathOnBindMount(c, workdir) { + if isPathOnVolume(c, workdir) || isPathOnMount(c, workdir) { logrus.Debugf("Workdir %q resolved to a volume or mount", workdir) return nil } @@ -788,7 +791,7 @@ func (c *Container) createCheckpointImage(ctx context.Context, options Container } // Export checkpoint into temporary tar file - tmpDir, err := ioutil.TempDir("", "checkpoint_image_") + tmpDir, err := os.MkdirTemp("", "checkpoint_image_") if err != nil { return err } @@ -2442,7 +2445,7 @@ func (c *Container) generatePasswdAndGroup() (string, string, error) { if err != nil { return "", "", fmt.Errorf("creating path to container %s /etc/passwd: %w", c.ID(), err) } - orig, err := ioutil.ReadFile(originPasswdFile) + orig, err := os.ReadFile(originPasswdFile) if err != nil && !os.IsNotExist(err) { return "", "", err } @@ -2488,7 +2491,7 @@ func (c *Container) generatePasswdAndGroup() (string, string, error) { if err != nil { return "", "", fmt.Errorf("creating path to container %s /etc/group: %w", c.ID(), err) } - orig, err := ioutil.ReadFile(originGroupFile) + orig, err := os.ReadFile(originGroupFile) if err != nil && !os.IsNotExist(err) { return "", "", err } @@ -2659,7 +2662,7 @@ func (c *Container) fixVolumePermissions(v *ContainerNamedVolume) error { return nil } -func (c *Container) relabel(src, mountLabel string, recurse bool) error { +func (c *Container) relabel(src, mountLabel string, shared bool) error { if !selinux.GetEnabled() || mountLabel == "" { return nil } @@ -2674,7 +2677,7 @@ func (c *Container) relabel(src, mountLabel string, recurse bool) error { return nil } } - return label.Relabel(src, mountLabel, recurse) + return label.Relabel(src, mountLabel, shared) } func (c *Container) ChangeHostPathOwnership(src string, recurse bool, uid, gid int) error { diff --git a/libpod/container_internal_test.go b/libpod/container_internal_test.go index 1b4e62e91..46a2da544 100644 --- a/libpod/container_internal_test.go +++ b/libpod/container_internal_test.go @@ -3,7 +3,7 @@ package libpod import ( "context" "fmt" - "io/ioutil" + "os" "path/filepath" "runtime" "testing" @@ -60,7 +60,7 @@ func TestPostDeleteHooks(t *testing.T) { for _, p := range []string{statePath, copyPath} { path := p t.Run(path, func(t *testing.T) { - content, err := ioutil.ReadFile(path) + content, err := os.ReadFile(path) if err != nil { t.Fatal(err) } diff --git a/libpod/container_path_resolution.go b/libpod/container_path_resolution.go index 35622d623..cd86df540 100644 --- a/libpod/container_path_resolution.go +++ b/libpod/container_path_resolution.go @@ -119,15 +119,29 @@ func findVolume(c *Container, containerPath string) (*Volume, error) { return nil, nil } +// isSubDir checks whether path is a subdirectory of root. +func isSubDir(path, root string) bool { + // check if the specified container path is below a bind mount. + rel, err := filepath.Rel(root, path) + if err != nil { + return false + } + return rel != ".." && !strings.HasPrefix(rel, "../") +} + // isPathOnVolume returns true if the specified containerPath is a subdir of any // Volume's destination. func isPathOnVolume(c *Container, containerPath string) bool { cleanedContainerPath := filepath.Clean(containerPath) for _, vol := range c.config.NamedVolumes { - if cleanedContainerPath == filepath.Clean(vol.Dest) { + cleanedDestination := filepath.Clean(vol.Dest) + if cleanedContainerPath == cleanedDestination { return true } - for dest := vol.Dest; dest != "/" && dest != "."; dest = filepath.Dir(dest) { + if isSubDir(cleanedContainerPath, cleanedDestination) { + return true + } + for dest := cleanedDestination; dest != "/" && dest != "."; dest = filepath.Dir(dest) { if cleanedContainerPath == dest { return true } @@ -152,15 +166,19 @@ func findBindMount(c *Container, containerPath string) *specs.Mount { return nil } -/// isPathOnBindMount returns true if the specified containerPath is a subdir of any +/// isPathOnMount returns true if the specified containerPath is a subdir of any // Mount's destination. -func isPathOnBindMount(c *Container, containerPath string) bool { +func isPathOnMount(c *Container, containerPath string) bool { cleanedContainerPath := filepath.Clean(containerPath) for _, m := range c.config.Spec.Mounts { - if cleanedContainerPath == filepath.Clean(m.Destination) { + cleanedDestination := filepath.Clean(m.Destination) + if cleanedContainerPath == cleanedDestination { + return true + } + if isSubDir(cleanedContainerPath, cleanedDestination) { return true } - for dest := m.Destination; dest != "/" && dest != "."; dest = filepath.Dir(dest) { + for dest := cleanedDestination; dest != "/" && dest != "."; dest = filepath.Dir(dest) { if cleanedContainerPath == dest { return true } diff --git a/libpod/container_path_resolution_test.go b/libpod/container_path_resolution_test.go new file mode 100644 index 000000000..f906c752d --- /dev/null +++ b/libpod/container_path_resolution_test.go @@ -0,0 +1,28 @@ +package libpod + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsSubDir(t *testing.T) { + assert.True(t, isSubDir("/foo", "/foo")) + assert.True(t, isSubDir("/foo/bar", "/foo")) + assert.True(t, isSubDir("/foo/bar", "/foo/")) + assert.True(t, isSubDir("/foo/bar", "/foo//")) + assert.True(t, isSubDir("/foo/bar/", "/foo")) + assert.True(t, isSubDir("/foo/bar/baz/", "/foo")) + assert.True(t, isSubDir("/foo/bar/baz/", "/foo/bar")) + assert.True(t, isSubDir("/foo/bar/baz/", "/foo/bar/")) + assert.False(t, isSubDir("/foo/bar/baz/", "/foobar/")) + assert.False(t, isSubDir("/foo/bar/baz/../../", "/foobar/")) + assert.False(t, isSubDir("/foo/bar/baz/", "../foo/bar")) + assert.False(t, isSubDir("/foo/bar/baz/", "../foo/")) + assert.False(t, isSubDir("/foo/bar/baz/", "../foo")) + assert.False(t, isSubDir("/", "..")) + assert.False(t, isSubDir("//", "..")) + assert.False(t, isSubDir("//", "../")) + assert.False(t, isSubDir("//", "..//")) + assert.True(t, isSubDir("/foo/bar/baz/../../", "/foo/")) +} diff --git a/libpod/container_stat_common.go b/libpod/container_stat_common.go new file mode 100644 index 000000000..e59a52ede --- /dev/null +++ b/libpod/container_stat_common.go @@ -0,0 +1,155 @@ +//go:build linux || freebsd +// +build linux freebsd + +package libpod + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/containers/buildah/copier" + "github.com/containers/podman/v4/libpod/define" + "github.com/containers/podman/v4/pkg/copy" +) + +// statOnHost stats the specified path *on the host*. It returns the file info +// along with the resolved root and the resolved path. Both paths are absolute +// to the host's root. Note that the paths may resolved outside the +// container's mount point (e.g., to a volume or bind mount). +func (c *Container) statOnHost(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) { + // Now resolve the container's path. It may hit a volume, it may hit a + // bind mount, it may be relative. + resolvedRoot, resolvedPath, err := c.resolvePath(mountPoint, containerPath) + if err != nil { + return nil, "", "", err + } + + statInfo, err := secureStat(resolvedRoot, resolvedPath) + return statInfo, resolvedRoot, resolvedPath, err +} + +func (c *Container) stat(containerMountPoint string, containerPath string) (*define.FileInfo, string, string, error) { + var ( + resolvedRoot string + resolvedPath string + absContainerPath string + statInfo *copier.StatForItem + statErr error + ) + + // Make sure that "/" copies the *contents* of the mount point and not + // the directory. + if containerPath == "/" { + containerPath = "/." + } + + // Wildcards are not allowed. + // TODO: it's now technically possible wildcards. + // We may consider enabling support in the future. + if strings.Contains(containerPath, "*") { + return nil, "", "", copy.ErrENOENT + } + + statInfo, resolvedRoot, resolvedPath, statErr = c.statInContainer(containerMountPoint, containerPath) + if statErr != nil { + if statInfo == nil { + return nil, "", "", statErr + } + // Not all errors from secureStat map to ErrNotExist, so we + // have to look into the error string. Turning it into an + // ENOENT let's the API handlers return the correct status code + // which is crucial for the remote client. + if os.IsNotExist(statErr) || strings.Contains(statErr.Error(), "o such file or directory") { + statErr = copy.ErrENOENT + } + } + + switch { + case statInfo.IsSymlink: + // Symlinks are already evaluated and always relative to the + // container's mount point. + absContainerPath = statInfo.ImmediateTarget + case strings.HasPrefix(resolvedPath, containerMountPoint): + // If the path is on the container's mount point, strip it off. + absContainerPath = strings.TrimPrefix(resolvedPath, containerMountPoint) + absContainerPath = filepath.Join("/", absContainerPath) + default: + // No symlink and not on the container's mount point, so let's + // move it back to the original input. It must have evaluated + // to a volume or bind mount but we cannot return host paths. + absContainerPath = containerPath + } + + // Preserve the base path as specified by the user. The `filepath` + // packages likes to remove trailing slashes and dots that are crucial + // to the copy logic. + absContainerPath = copy.PreserveBasePath(containerPath, absContainerPath) + resolvedPath = copy.PreserveBasePath(containerPath, resolvedPath) + + info := &define.FileInfo{ + IsDir: statInfo.IsDir, + Name: filepath.Base(absContainerPath), + Size: statInfo.Size, + Mode: statInfo.Mode, + ModTime: statInfo.ModTime, + LinkTarget: absContainerPath, + } + + return info, resolvedRoot, resolvedPath, statErr +} + +// secureStat extracts file info for path in a chroot'ed environment in root. +func secureStat(root string, path string) (*copier.StatForItem, error) { + var glob string + var err error + + // If root and path are equal, then dir must be empty and the glob must + // be ".". + if filepath.Clean(root) == filepath.Clean(path) { + glob = "." + } else { + glob, err = filepath.Rel(root, path) + if err != nil { + return nil, err + } + } + + globStats, err := copier.Stat(root, "", copier.StatOptions{}, []string{glob}) + if err != nil { + return nil, err + } + + if len(globStats) != 1 { + return nil, fmt.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats)) + } + if len(globStats) != 1 { + return nil, fmt.Errorf("internal error: secureStat: expected 1 result but got %d", len(globStats[0].Results)) + } + + // NOTE: the key in the map differ from `glob` when hitting symlink. + // Hence, we just take the first (and only) key/value pair. + for _, stat := range globStats[0].Results { + var statErr error + if stat.Error != "" { + statErr = errors.New(stat.Error) + } + // If necessary evaluate the symlink + if stat.IsSymlink { + target, err := copier.Eval(root, path, copier.EvalOptions{}) + if err != nil { + return nil, fmt.Errorf("evaluating symlink in container: %w", err) + } + // Need to make sure the symlink is relative to the root! + target = strings.TrimPrefix(target, root) + target = filepath.Join("/", target) + stat.ImmediateTarget = target + } + return stat, statErr + } + + // Nothing found! + return nil, copy.ErrENOENT +} diff --git a/libpod/container_stat_freebsd.go b/libpod/container_stat_freebsd.go new file mode 100644 index 000000000..d1e0db348 --- /dev/null +++ b/libpod/container_stat_freebsd.go @@ -0,0 +1,13 @@ +package libpod + +import ( + "github.com/containers/buildah/copier" +) + +// On FreeBSD, jails use the global mount namespace, filtered to only +// the mounts the jail should see. This means that we can use +// statOnHost whether the container is running or not. +// container is running +func (c *Container) statInContainer(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) { + return c.statOnHost(mountPoint, containerPath) +} diff --git a/libpod/container_stat_linux.go b/libpod/container_stat_linux.go index dc3a524f5..5e5ef3c1a 100644 --- a/libpod/container_stat_linux.go +++ b/libpod/container_stat_linux.go @@ -1,18 +1,8 @@ -//go:build linux -// +build linux - package libpod import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "github.com/containers/buildah/copier" "github.com/containers/podman/v4/libpod/define" - "github.com/containers/podman/v4/pkg/copy" ) // statInsideMount stats the specified path *inside* the container's mount and PID @@ -34,150 +24,15 @@ func (c *Container) statInsideMount(containerPath string) (*copier.StatForItem, return statInfo, resolvedRoot, resolvedPath, err } -// statOnHost stats the specified path *on the host*. It returns the file info -// along with the resolved root and the resolved path. Both paths are absolute -// to the host's root. Note that the paths may resolved outside the -// container's mount point (e.g., to a volume or bind mount). -func (c *Container) statOnHost(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) { - // Now resolve the container's path. It may hit a volume, it may hit a - // bind mount, it may be relative. - resolvedRoot, resolvedPath, err := c.resolvePath(mountPoint, containerPath) - if err != nil { - return nil, "", "", err - } - - statInfo, err := secureStat(resolvedRoot, resolvedPath) - return statInfo, resolvedRoot, resolvedPath, err -} - -func (c *Container) stat(containerMountPoint string, containerPath string) (*define.FileInfo, string, string, error) { - var ( - resolvedRoot string - resolvedPath string - absContainerPath string - statInfo *copier.StatForItem - statErr error - ) - - // Make sure that "/" copies the *contents* of the mount point and not - // the directory. - if containerPath == "/" { - containerPath = "/." - } - - // Wildcards are not allowed. - // TODO: it's now technically possible wildcards. - // We may consider enabling support in the future. - if strings.Contains(containerPath, "*") { - return nil, "", "", copy.ErrENOENT - } - +// Calls either statOnHost or statInsideMount depending on whether the +// container is running +func (c *Container) statInContainer(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) { if c.state.State == define.ContainerStateRunning { // If the container is running, we need to join it's mount namespace // and stat there. - statInfo, resolvedRoot, resolvedPath, statErr = c.statInsideMount(containerPath) - } else { - // If the container is NOT running, we need to resolve the path - // on the host. - statInfo, resolvedRoot, resolvedPath, statErr = c.statOnHost(containerMountPoint, containerPath) - } - - if statErr != nil { - if statInfo == nil { - return nil, "", "", statErr - } - // Not all errors from secureStat map to ErrNotExist, so we - // have to look into the error string. Turning it into an - // ENOENT let's the API handlers return the correct status code - // which is crucial for the remote client. - if os.IsNotExist(statErr) || strings.Contains(statErr.Error(), "o such file or directory") { - statErr = copy.ErrENOENT - } - } - - switch { - case statInfo.IsSymlink: - // Symlinks are already evaluated and always relative to the - // container's mount point. - absContainerPath = statInfo.ImmediateTarget - case strings.HasPrefix(resolvedPath, containerMountPoint): - // If the path is on the container's mount point, strip it off. - absContainerPath = strings.TrimPrefix(resolvedPath, containerMountPoint) - absContainerPath = filepath.Join("/", absContainerPath) - default: - // No symlink and not on the container's mount point, so let's - // move it back to the original input. It must have evaluated - // to a volume or bind mount but we cannot return host paths. - absContainerPath = containerPath + return c.statInsideMount(containerPath) } - - // Preserve the base path as specified by the user. The `filepath` - // packages likes to remove trailing slashes and dots that are crucial - // to the copy logic. - absContainerPath = copy.PreserveBasePath(containerPath, absContainerPath) - resolvedPath = copy.PreserveBasePath(containerPath, resolvedPath) - - info := &define.FileInfo{ - IsDir: statInfo.IsDir, - Name: filepath.Base(absContainerPath), - Size: statInfo.Size, - Mode: statInfo.Mode, - ModTime: statInfo.ModTime, - LinkTarget: absContainerPath, - } - - return info, resolvedRoot, resolvedPath, statErr -} - -// secureStat extracts file info for path in a chroot'ed environment in root. -func secureStat(root string, path string) (*copier.StatForItem, error) { - var glob string - var err error - - // If root and path are equal, then dir must be empty and the glob must - // be ".". - if filepath.Clean(root) == filepath.Clean(path) { - glob = "." - } else { - glob, err = filepath.Rel(root, path) - if err != nil { - return nil, err - } - } - - globStats, err := copier.Stat(root, "", copier.StatOptions{}, []string{glob}) - if err != nil { - return nil, err - } - - if len(globStats) != 1 { - return nil, fmt.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats)) - } - if len(globStats) != 1 { - return nil, fmt.Errorf("internal error: secureStat: expected 1 result but got %d", len(globStats[0].Results)) - } - - // NOTE: the key in the map differ from `glob` when hitting symlink. - // Hence, we just take the first (and only) key/value pair. - for _, stat := range globStats[0].Results { - var statErr error - if stat.Error != "" { - statErr = errors.New(stat.Error) - } - // If necessary evaluate the symlink - if stat.IsSymlink { - target, err := copier.Eval(root, path, copier.EvalOptions{}) - if err != nil { - return nil, fmt.Errorf("evaluating symlink in container: %w", err) - } - // Need to make sure the symlink is relative to the root! - target = strings.TrimPrefix(target, root) - target = filepath.Join("/", target) - stat.ImmediateTarget = target - } - return stat, statErr - } - - // Nothing found! - return nil, copy.ErrENOENT + // If the container is NOT running, we need to resolve the path + // on the host. + return c.statOnHost(mountPoint, containerPath) } diff --git a/libpod/container_stat_unsupported.go b/libpod/container_stat_unsupported.go index 2f1acd44d..e88b88bb1 100644 --- a/libpod/container_stat_unsupported.go +++ b/libpod/container_stat_unsupported.go @@ -1,5 +1,5 @@ -//go:build !linux -// +build !linux +//go:build !linux && !freebsd +// +build !linux,!freebsd package libpod diff --git a/libpod/define/config.go b/libpod/define/config.go index 1fad5cc9a..0427206ed 100644 --- a/libpod/define/config.go +++ b/libpod/define/config.go @@ -40,6 +40,10 @@ type InfoData struct { // itself. const VolumeDriverLocal = "local" +// VolumeDriverImage is the "image" volume driver. It is managed by Libpod and +// uses volumes backed by an image. +const VolumeDriverImage = "image" + const ( OCIManifestDir = "oci-dir" OCIArchive = "oci-archive" diff --git a/libpod/define/volume_inspect.go b/libpod/define/volume_inspect.go index 76120647c..4d6f12080 100644 --- a/libpod/define/volume_inspect.go +++ b/libpod/define/volume_inspect.go @@ -58,6 +58,9 @@ type InspectVolumeData struct { NeedsChown bool `json:"NeedsChown,omitempty"` // Timeout is the specified driver timeout if given Timeout uint `json:"Timeout,omitempty"` + // StorageID is the ID of the container backing the volume in c/storage. + // Only used with Image Volumes. + StorageID string `json:"StorageID,omitempty"` } type VolumeReload struct { diff --git a/libpod/events.go b/libpod/events.go index 2f9799114..31a857a7d 100644 --- a/libpod/events.go +++ b/libpod/events.go @@ -34,6 +34,7 @@ func (c *Container) newContainerEvent(status events.Status) { e.Details = events.Details{ ID: e.ID, + PodID: c.PodID(), Attributes: c.Labels(), } @@ -59,6 +60,7 @@ func (c *Container) newContainerExitedEvent(exitCode int32) { e.Name = c.Name() e.Image = c.config.RootfsImageName e.Type = events.Container + e.PodID = c.PodID() e.ContainerExitCode = int(exitCode) e.Details = events.Details{ diff --git a/libpod/events/config.go b/libpod/events/config.go index 4ea45a00e..28bc87a34 100644 --- a/libpod/events/config.go +++ b/libpod/events/config.go @@ -50,6 +50,8 @@ type Event struct { type Details struct { // ID is the event ID ID string + // PodID is the ID of the pod associated with the container. + PodID string `json:",omitempty"` // Attributes can be used to describe specifics about the event // in the case of a container event, labels for example Attributes map[string]string diff --git a/libpod/events/events.go b/libpod/events/events.go index 764481e51..2105a3b89 100644 --- a/libpod/events/events.go +++ b/libpod/events/events.go @@ -76,7 +76,13 @@ func (e *Event) ToHumanReadable(truncate bool) string { } switch e.Type { case Container, Pod: - humanFormat = fmt.Sprintf("%s %s %s %s (image=%s, name=%s, health_status=%s", e.Time, e.Type, e.Status, id, e.Image, e.Name, e.HealthStatus) + humanFormat = fmt.Sprintf("%s %s %s %s (image=%s, name=%s", e.Time, e.Type, e.Status, id, e.Image, e.Name) + if e.PodID != "" { + humanFormat += fmt.Sprintf(", pod_id=%s", e.PodID) + } + if e.HealthStatus != "" { + humanFormat += fmt.Sprintf(", health_status=%s", e.HealthStatus) + } // check if the container has labels and add it to the output if len(e.Attributes) > 0 { for k, v := range e.Attributes { diff --git a/libpod/events/journal_linux.go b/libpod/events/journal_linux.go index 4986502a2..e303a205d 100644 --- a/libpod/events/journal_linux.go +++ b/libpod/events/journal_linux.go @@ -50,6 +50,9 @@ func (e EventJournalD) Write(ee Event) error { if ee.ContainerExitCode != 0 { m["PODMAN_EXIT_CODE"] = strconv.Itoa(ee.ContainerExitCode) } + if ee.PodID != "" { + m["PODMAN_POD_ID"] = ee.PodID + } // If we have container labels, we need to convert them to a string so they // can be recorded with the event if len(ee.Details.Attributes) > 0 { @@ -161,6 +164,7 @@ func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { case Container, Pod: newEvent.ID = entry.Fields["PODMAN_ID"] newEvent.Image = entry.Fields["PODMAN_IMAGE"] + newEvent.PodID = entry.Fields["PODMAN_POD_ID"] if code, ok := entry.Fields["PODMAN_EXIT_CODE"]; ok { intCode, err := strconv.Atoi(code) if err != nil { @@ -179,7 +183,7 @@ func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { // if we have labels, add them to the event if len(labels) > 0 { - newEvent.Details = Details{Attributes: labels} + newEvent.Attributes = labels } } newEvent.HealthStatus = entry.Fields["PODMAN_HEALTH_STATUS"] diff --git a/libpod/events/logfile.go b/libpod/events/logfile.go index d749a0d4d..bb0f461e3 100644 --- a/libpod/events/logfile.go +++ b/libpod/events/logfile.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "path" "path/filepath" @@ -204,11 +203,11 @@ func truncate(filePath string) error { size := origFinfo.Size() threshold := size / 2 - tmp, err := ioutil.TempFile(path.Dir(filePath), "") + tmp, err := os.CreateTemp(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("", "") + tmp, err = os.CreateTemp("", "") if err != nil { return err } diff --git a/libpod/events/logfile_test.go b/libpod/events/logfile_test.go index 302533c12..50141168e 100644 --- a/libpod/events/logfile_test.go +++ b/libpod/events/logfile_test.go @@ -1,7 +1,6 @@ package events import ( - "io/ioutil" "os" "testing" @@ -29,7 +28,7 @@ func TestRotateLog(t *testing.T) { } for _, test := range tests { - tmp, err := ioutil.TempFile("", "log-rotation-") + tmp, err := os.CreateTemp("", "log-rotation-") require.NoError(t, err) defer os.Remove(tmp.Name()) defer tmp.Close() @@ -84,7 +83,7 @@ func TestTruncationOutput(t *testing.T) { 10 ` // Create dummy file - tmp, err := ioutil.TempFile("", "log-rotation") + tmp, err := os.CreateTemp("", "log-rotation") require.NoError(t, err) defer os.Remove(tmp.Name()) defer tmp.Close() @@ -94,11 +93,11 @@ func TestTruncationOutput(t *testing.T) { require.NoError(t, err) // Truncate the file - beforeTruncation, err := ioutil.ReadFile(tmp.Name()) + beforeTruncation, err := os.ReadFile(tmp.Name()) require.NoError(t, err) err = truncate(tmp.Name()) require.NoError(t, err) - afterTruncation, err := ioutil.ReadFile(tmp.Name()) + afterTruncation, err := os.ReadFile(tmp.Name()) require.NoError(t, err) // Test if rotation was successful @@ -116,9 +115,9 @@ func TestRenameLog(t *testing.T) { 5 ` // Create two dummy files - source, err := ioutil.TempFile("", "removing") + source, err := os.CreateTemp("", "removing") require.NoError(t, err) - target, err := ioutil.TempFile("", "renaming") + target, err := os.CreateTemp("", "renaming") require.NoError(t, err) // Write to source dummy file @@ -126,11 +125,11 @@ func TestRenameLog(t *testing.T) { require.NoError(t, err) // Rename the files - beforeRename, err := ioutil.ReadFile(source.Name()) + beforeRename, err := os.ReadFile(source.Name()) require.NoError(t, err) err = renameLog(source.Name(), target.Name()) require.NoError(t, err) - afterRename, err := ioutil.ReadFile(target.Name()) + afterRename, err := os.ReadFile(target.Name()) require.NoError(t, err) // Test if renaming was successful diff --git a/libpod/healthcheck.go b/libpod/healthcheck.go index e835af9f0..a589f2787 100644 --- a/libpod/healthcheck.go +++ b/libpod/healthcheck.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -208,7 +207,7 @@ func (c *Container) updateHealthStatus(status string) error { if err != nil { return fmt.Errorf("unable to marshall healthchecks for writing status: %w", err) } - return ioutil.WriteFile(c.healthCheckLogPath(), newResults, 0700) + return os.WriteFile(c.healthCheckLogPath(), newResults, 0700) } // UpdateHealthCheckLog parses the health check results and writes the log @@ -242,7 +241,7 @@ func (c *Container) updateHealthCheckLog(hcl define.HealthCheckLog, inStartPerio if err != nil { return fmt.Errorf("unable to marshall healthchecks for writing: %w", err) } - return ioutil.WriteFile(c.healthCheckLogPath(), newResults, 0700) + return os.WriteFile(c.healthCheckLogPath(), newResults, 0700) } // HealthCheckLogPath returns the path for where the health check log is @@ -259,7 +258,7 @@ func (c *Container) getHealthCheckLog() (define.HealthCheckResults, error) { if _, err := os.Stat(c.healthCheckLogPath()); os.IsNotExist(err) { return healthCheck, nil } - b, err := ioutil.ReadFile(c.healthCheckLogPath()) + b, err := os.ReadFile(c.healthCheckLogPath()) if err != nil { return healthCheck, fmt.Errorf("failed to read health check log file: %w", err) } diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index e27ec8e9d..6ea56ade5 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -8,7 +8,6 @@ import ( "crypto/sha256" "errors" "fmt" - "io/ioutil" "net" "os" "os/exec" @@ -303,7 +302,7 @@ func (r *RootlessNetNS) Cleanup(runtime *Runtime) error { if err != nil { logrus.Error(err) } - b, err := ioutil.ReadFile(r.getPath(rootlessNetNsSilrp4netnsPidFile)) + b, err := os.ReadFile(r.getPath(rootlessNetNsSilrp4netnsPidFile)) if err == nil { var i int i, err = strconv.Atoi(string(b)) @@ -445,7 +444,7 @@ func (r *Runtime) GetRootlessNetNs(new bool) (*RootlessNetNS, error) { // create pid file for the slirp4netns process // this is need to kill the process in the cleanup pid := strconv.Itoa(cmd.Process.Pid) - err = ioutil.WriteFile(filepath.Join(rootlessNetNsDir, rootlessNetNsSilrp4netnsPidFile), []byte(pid), 0700) + err = os.WriteFile(filepath.Join(rootlessNetNsDir, rootlessNetNsSilrp4netnsPidFile), []byte(pid), 0700) if err != nil { return nil, fmt.Errorf("unable to write rootless-netns slirp4netns pid file: %w", err) } diff --git a/libpod/networking_machine.go b/libpod/networking_machine.go index 7b8eb94df..dce335c0a 100644 --- a/libpod/networking_machine.go +++ b/libpod/networking_machine.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "strconv" @@ -109,7 +108,7 @@ func makeMachineRequest(ctx context.Context, client *http.Client, url string, bu } func annotateGvproxyResponseError(r io.Reader) error { - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err == nil && len(b) > 0 { return fmt.Errorf("something went wrong with the request: %q", string(b)) } diff --git a/libpod/networking_slirp4netns.go b/libpod/networking_slirp4netns.go index d4ec9082b..4026b6b48 100644 --- a/libpod/networking_slirp4netns.go +++ b/libpod/networking_slirp4netns.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "os" "os/exec" @@ -324,7 +323,7 @@ func (r *Runtime) setupSlirp4netns(ctr *Container, netns ns.NetNS) error { // correct value assigned so DAD is disabled for it // Also make sure to change this value back to the original after slirp4netns // is ready in case users rely on this sysctl. - orgValue, err := ioutil.ReadFile(ipv6ConfDefaultAcceptDadSysctl) + orgValue, err := os.ReadFile(ipv6ConfDefaultAcceptDadSysctl) if err != nil { netnsReadyWg.Done() // on ipv6 disabled systems the sysctl does not exists @@ -334,7 +333,7 @@ func (r *Runtime) setupSlirp4netns(ctr *Container, netns ns.NetNS) error { } return err } - err = ioutil.WriteFile(ipv6ConfDefaultAcceptDadSysctl, []byte("0"), 0644) + err = os.WriteFile(ipv6ConfDefaultAcceptDadSysctl, []byte("0"), 0644) netnsReadyWg.Done() if err != nil { return err @@ -342,7 +341,7 @@ func (r *Runtime) setupSlirp4netns(ctr *Container, netns ns.NetNS) error { // wait until slirp4nets is ready before resetting this value slirpReadyWg.Wait() - return ioutil.WriteFile(ipv6ConfDefaultAcceptDadSysctl, orgValue, 0644) + return os.WriteFile(ipv6ConfDefaultAcceptDadSysctl, orgValue, 0644) }) if err != nil { logrus.Warnf("failed to set net.ipv6.conf.default.accept_dad sysctl: %v", err) @@ -486,7 +485,7 @@ func waitForSync(syncR *os.File, cmd *exec.Cmd, logFile io.ReadSeeker, timeout t if _, err := logFile.Seek(0, 0); err != nil { logrus.Errorf("Could not seek log file: %q", err) } - logContent, err := ioutil.ReadAll(logFile) + logContent, err := io.ReadAll(logFile) if err != nil { return fmt.Errorf("%s failed: %w", prog, err) } @@ -730,7 +729,7 @@ func (c *Container) reloadRootlessRLKPortMapping() error { if err != nil { return fmt.Errorf("port reloading failed: %w", err) } - b, err := ioutil.ReadAll(conn) + b, err := io.ReadAll(conn) if err != nil { return fmt.Errorf("port reloading failed: %w", err) } diff --git a/libpod/oci_conmon_common.go b/libpod/oci_conmon_common.go index 53dddd064..cbdbad02d 100644 --- a/libpod/oci_conmon_common.go +++ b/libpod/oci_conmon_common.go @@ -10,7 +10,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "os" @@ -232,7 +231,7 @@ func (r *ConmonOCIRuntime) UpdateContainerStatus(ctr *Container) error { } if err := cmd.Start(); err != nil { - out, err2 := ioutil.ReadAll(errPipe) + out, err2 := io.ReadAll(errPipe) if err2 != nil { return fmt.Errorf("getting container %s state: %w", ctr.ID(), err) } @@ -254,7 +253,7 @@ func (r *ConmonOCIRuntime) UpdateContainerStatus(ctr *Container) error { if err := errPipe.Close(); err != nil { return err } - out, err := ioutil.ReadAll(outPipe) + out, err := io.ReadAll(outPipe) if err != nil { return fmt.Errorf("reading stdout: %s: %w", ctr.ID(), err) } @@ -335,7 +334,7 @@ func generateResourceFile(res *spec.LinuxResources) (string, []string, error) { return "", flags, nil } - f, err := ioutil.TempFile("", "podman") + f, err := os.CreateTemp("", "podman") if err != nil { return "", nil, err } @@ -1398,7 +1397,7 @@ func newPipe() (*os.File, *os.File, error) { func readConmonPidFile(pidFile string) (int, error) { // Let's try reading the Conmon pid at the same time. if pidFile != "" { - contents, err := ioutil.ReadFile(pidFile) + contents, err := os.ReadFile(pidFile) if err != nil { return -1, err } @@ -1447,7 +1446,7 @@ func readConmonPipeData(runtimeName string, pipe *os.File, ociLog string) (int, case ss := <-ch: if ss.err != nil { if ociLog != "" { - ociLogData, err := ioutil.ReadFile(ociLog) + ociLogData, err := os.ReadFile(ociLog) if err == nil { var ociErr ociError if err := json.Unmarshal(ociLogData, &ociErr); err == nil { @@ -1460,7 +1459,7 @@ func readConmonPipeData(runtimeName string, pipe *os.File, ociLog string) (int, logrus.Debugf("Received: %d", ss.si.Data) if ss.si.Data < 0 { if ociLog != "" { - ociLogData, err := ioutil.ReadFile(ociLog) + ociLogData, err := os.ReadFile(ociLog) if err == nil { var ociErr ociError if err := json.Unmarshal(ociLogData, &ociErr); err == nil { diff --git a/libpod/oci_conmon_exec_common.go b/libpod/oci_conmon_exec_common.go index e5080942b..24113bd8d 100644 --- a/libpod/oci_conmon_exec_common.go +++ b/libpod/oci_conmon_exec_common.go @@ -3,7 +3,6 @@ package libpod import ( "errors" "fmt" - "io/ioutil" "net/http" "os" "os/exec" @@ -665,7 +664,7 @@ func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.Resp // prepareProcessExec returns the path of the process.json used in runc exec -p // caller is responsible to close the returned *os.File if needed. func (c *Container) prepareProcessExec(options *ExecOptions, env []string, sessionID string) (*os.File, error) { - f, err := ioutil.TempFile(c.execBundlePath(sessionID), "exec-process-") + f, err := os.CreateTemp(c.execBundlePath(sessionID), "exec-process-") if err != nil { return nil, err } @@ -764,7 +763,7 @@ func (c *Container) prepareProcessExec(options *ExecOptions, env []string, sessi return nil, err } - if err := ioutil.WriteFile(f.Name(), processJSON, 0644); err != nil { + if err := os.WriteFile(f.Name(), processJSON, 0644); err != nil { return nil, err } return f, nil diff --git a/libpod/plugin/volume_api.go b/libpod/plugin/volume_api.go index 522895798..c595937ae 100644 --- a/libpod/plugin/volume_api.go +++ b/libpod/plugin/volume_api.go @@ -5,7 +5,7 @@ import ( "context" "errors" "fmt" - "io/ioutil" + "io" "net" "net/http" "os" @@ -95,7 +95,7 @@ func validatePlugin(newPlugin *VolumePlugin) error { } // Read and decode the body so we can tell if this is a volume plugin. - respBytes, err := ioutil.ReadAll(resp.Body) + respBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("reading activation response body from plugin %s: %w", newPlugin.Name, err) } @@ -252,7 +252,7 @@ func (p *VolumePlugin) handleErrorResponse(resp *http.Response, endpoint, volNam // Let's interpret anything other than 200 as an error. // If there isn't an error, don't even bother decoding the response. if resp.StatusCode != 200 { - errResp, err := ioutil.ReadAll(resp.Body) + errResp, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("reading response body from volume plugin %s: %w", p.Name, err) } @@ -307,7 +307,7 @@ func (p *VolumePlugin) ListVolumes() ([]*volume.Volume, error) { return nil, err } - volumeRespBytes, err := ioutil.ReadAll(resp.Body) + volumeRespBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("reading response body from volume plugin %s: %w", p.Name, err) } @@ -342,7 +342,7 @@ func (p *VolumePlugin) GetVolume(req *volume.GetRequest) (*volume.Volume, error) return nil, err } - getRespBytes, err := ioutil.ReadAll(resp.Body) + getRespBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("reading response body from volume plugin %s: %w", p.Name, err) } @@ -398,7 +398,7 @@ func (p *VolumePlugin) GetVolumePath(req *volume.PathRequest) (string, error) { return "", err } - pathRespBytes, err := ioutil.ReadAll(resp.Body) + pathRespBytes, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("reading response body from volume plugin %s: %w", p.Name, err) } @@ -435,7 +435,7 @@ func (p *VolumePlugin) MountVolume(req *volume.MountRequest) (string, error) { return "", err } - mountRespBytes, err := ioutil.ReadAll(resp.Body) + mountRespBytes, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("reading response body from volume plugin %s: %w", p.Name, err) } diff --git a/libpod/pod_api.go b/libpod/pod_api.go index 1bd686ddc..924d43436 100644 --- a/libpod/pod_api.go +++ b/libpod/pod_api.go @@ -40,7 +40,7 @@ func (p *Pod) startInitContainers(ctx context.Context) error { icLock := initCon.lock icLock.Lock() var time *uint - if err := p.runtime.removeContainer(ctx, initCon, false, false, true, time); err != nil { + if err := p.runtime.removeContainer(ctx, initCon, false, false, true, false, time); err != nil { icLock.Unlock() return fmt.Errorf("failed to remove once init container %s: %w", initCon.ID(), err) } diff --git a/libpod/runtime.go b/libpod/runtime.go index 83c9f53e2..f250d759c 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -1091,6 +1091,9 @@ func (r *Runtime) getVolumePlugin(volConfig *VolumeConfig) (*plugin.VolumePlugin pluginPath, ok := r.config.Engine.VolumePlugins[name] if !ok { + if name == define.VolumeDriverImage { + return nil, nil + } return nil, fmt.Errorf("no volume plugin with name %s available: %w", name, define.ErrMissingPlugin) } diff --git a/libpod/runtime_cstorage.go b/libpod/runtime_cstorage.go index 372434b49..5917b7931 100644 --- a/libpod/runtime_cstorage.go +++ b/libpod/runtime_cstorage.go @@ -86,6 +86,17 @@ func (r *Runtime) RemoveStorageContainer(idOrName string, force bool) error { return fmt.Errorf("refusing to remove %q as it exists in libpod as container %s: %w", idOrName, ctr.ID, define.ErrCtrExists) } + // Error out if this is an image-backed volume + allVols, err := r.state.AllVolumes() + if err != nil { + return err + } + for _, vol := range allVols { + if vol.config.Driver == define.VolumeDriverImage && vol.config.StorageID == ctr.ID { + return fmt.Errorf("refusing to remove %q as it exists in libpod as an image-backed volume %s: %w", idOrName, vol.Name(), define.ErrCtrExists) + } + } + if !force { timesMounted, err := r.store.Mounted(ctr.ID) if err != nil { diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 1f032dd6b..bb30078cb 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -512,7 +512,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai volOptions = append(volOptions, parsedOptions...) } } - newVol, err := r.newVolume(false, volOptions...) + newVol, err := r.newVolume(ctx, false, volOptions...) if err != nil { return nil, fmt.Errorf("creating named volume %q: %w", vol.Name, err) } @@ -581,7 +581,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai // be removed also if and only if the container is the sole user // Otherwise, RemoveContainer will return an error if the container is running func (r *Runtime) RemoveContainer(ctx context.Context, c *Container, force bool, removeVolume bool, timeout *uint) error { - return r.removeContainer(ctx, c, force, removeVolume, false, timeout) + return r.removeContainer(ctx, c, force, removeVolume, false, false, timeout) } // Internal function to remove a container. @@ -589,7 +589,9 @@ func (r *Runtime) RemoveContainer(ctx context.Context, c *Container, force bool, // removePod is used only when removing pods. It instructs Podman to ignore // infra container protections, and *not* remove from the database (as pod // remove will handle that). -func (r *Runtime) removeContainer(ctx context.Context, c *Container, force, removeVolume, removePod bool, timeout *uint) error { +// ignoreDeps is *DANGEROUS* and should not be used outside of a very specific +// context (alternate pod removal code, where graph traversal is not possible). +func (r *Runtime) removeContainer(ctx context.Context, c *Container, force, removeVolume, removePod, ignoreDeps bool, timeout *uint) error { if !c.valid { if ok, _ := r.state.HasContainer(c.ID()); !ok { // Container probably already removed @@ -618,25 +620,27 @@ func (r *Runtime) removeContainer(ctx context.Context, c *Container, force, remo // pod. var pod *Pod runtime := c.runtime - if c.config.Pod != "" && !removePod { + if c.config.Pod != "" { pod, err = r.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(), pod.ID(), err) } - // Lock the pod while we're removing container - if pod.config.LockID == c.config.LockID { - return fmt.Errorf("container %s and pod %s share lock ID %d: %w", c.ID(), pod.ID(), c.config.LockID, define.ErrWillDeadlock) - } - pod.lock.Lock() - defer pod.lock.Unlock() - if err := pod.updatePod(); err != nil { - return err - } + if !removePod { + // Lock the pod while we're removing container + if pod.config.LockID == c.config.LockID { + return fmt.Errorf("container %s and pod %s share lock ID %d: %w", c.ID(), pod.ID(), c.config.LockID, define.ErrWillDeadlock) + } + pod.lock.Lock() + defer pod.lock.Unlock() + if err := pod.updatePod(); err != nil { + return err + } - infraID := pod.state.InfraContainerID - if c.ID() == infraID { - return fmt.Errorf("container %s is the infra container of pod %s and cannot be removed without removing the pod", c.ID(), pod.ID()) + infraID := pod.state.InfraContainerID + if c.ID() == infraID { + return fmt.Errorf("container %s is the infra container of pod %s and cannot be removed without removing the pod", c.ID(), pod.ID()) + } } } @@ -696,7 +700,7 @@ func (r *Runtime) removeContainer(ctx context.Context, c *Container, force, remo // Check that no other containers depend on the container. // Only used if not removing a pod - pods guarantee that all // deps will be evicted at the same time. - if !removePod { + if !ignoreDeps { deps, err := r.state.ContainerInUse(c) if err != nil { return err @@ -777,13 +781,11 @@ func (r *Runtime) removeContainer(ctx context.Context, c *Container, force, remo if c.config.Pod != "" { // If we're removing the pod, the container will be evicted // from the state elsewhere - if !removePod { - if err := r.state.RemoveContainerFromPod(pod, c); err != nil { - if cleanupErr == nil { - cleanupErr = err - } else { - logrus.Errorf("Removing container %s from database: %v", c.ID(), err) - } + if err := r.state.RemoveContainerFromPod(pod, c); err != nil { + if cleanupErr == nil { + cleanupErr = err + } else { + logrus.Errorf("Removing container %s from database: %v", c.ID(), err) } } } else { @@ -872,7 +874,7 @@ func (r *Runtime) evictContainer(ctx context.Context, idOrName string, removeVol if err == nil { logrus.Infof("Container %s successfully retrieved from state, attempting normal removal", id) // Assume force = true for the evict case - err = r.removeContainer(ctx, tmpCtr, true, removeVolume, false, timeout) + err = r.removeContainer(ctx, tmpCtr, true, removeVolume, false, false, timeout) if !tmpCtr.valid { // If the container is marked invalid, remove succeeded // in kicking it out of the state - no need to continue. @@ -1034,7 +1036,7 @@ func (r *Runtime) RemoveDepend(ctx context.Context, rmCtr *Container, force bool } report := reports.RmReport{Id: rmCtr.ID(), RawInput: rmCtr.ID()} - report.Err = r.removeContainer(ctx, rmCtr, force, removeVolume, false, timeout) + report.Err = r.removeContainer(ctx, rmCtr, force, removeVolume, false, false, timeout) return append(rmReports, &report), nil } diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index 5510b2af6..d8e88ca50 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" buildahDefine "github.com/containers/buildah/define" @@ -47,11 +46,28 @@ func (r *Runtime) RemoveContainersForImageCallback(ctx context.Context) libimage return fmt.Errorf("removing image %s: container %s using image could not be removed: %w", imageID, ctr.ID(), err) } } else { - if err := r.removeContainer(ctx, ctr, true, false, false, timeout); err != nil { + if err := r.removeContainer(ctx, ctr, true, false, false, false, timeout); err != nil { return fmt.Errorf("removing image %s: container %s using image could not be removed: %w", imageID, ctr.ID(), err) } } } + + // Need to handle volumes with the image driver + vols, err := r.state.AllVolumes() + if err != nil { + return err + } + for _, vol := range vols { + if vol.config.Driver != define.VolumeDriverImage || vol.config.StorageImageID != imageID { + continue + } + // Do a force removal of the volume, and all containers + // using it. + if err := r.RemoveVolume(ctx, vol, true, nil); err != nil { + return fmt.Errorf("removing image %s: volume %s backed by image could not be removed: %w", imageID, vol.Name(), err) + } + } + // Note that `libimage` will take care of removing any leftover // containers from the storage. return nil @@ -75,6 +91,10 @@ func (r *Runtime) IsExternalContainerCallback(_ context.Context) libimage.IsExte if errors.Is(err, define.ErrNoSuchCtr) { return true, nil } + isVol, err := r.state.ContainerIDIsVolume(idOrName) + if err == nil && !isVol { + return true, nil + } return false, nil } } @@ -105,7 +125,7 @@ func (r *Runtime) Build(ctx context.Context, options buildahDefine.BuildOptions, // DownloadFromFile reads all of the content from the reader and temporarily // saves in it $TMPDIR/importxyz, which is deleted after the image is imported func DownloadFromFile(reader *os.File) (string, error) { - outFile, err := ioutil.TempFile(util.Tmpdir(), "import") + outFile, err := os.CreateTemp(util.Tmpdir(), "import") if err != nil { return "", fmt.Errorf("creating file: %w", err) } diff --git a/libpod/runtime_migrate.go b/libpod/runtime_migrate.go index 36901d4d0..df1a1f1cb 100644 --- a/libpod/runtime_migrate.go +++ b/libpod/runtime_migrate.go @@ -5,7 +5,6 @@ package libpod import ( "fmt" - "io/ioutil" "os" "path/filepath" "strconv" @@ -23,7 +22,7 @@ func (r *Runtime) stopPauseProcess() error { if err != nil { return fmt.Errorf("could not get pause process pid file path: %w", err) } - data, err := ioutil.ReadFile(pausePidPath) + data, err := os.ReadFile(pausePidPath) if err != nil { if os.IsNotExist(err) { return nil diff --git a/libpod/runtime_pod_linux.go b/libpod/runtime_pod_linux.go index 3eeef69d8..24e9f3da7 100644 --- a/libpod/runtime_pod_linux.go +++ b/libpod/runtime_pod_linux.go @@ -17,6 +17,7 @@ import ( "github.com/containers/podman/v4/libpod/events" "github.com/containers/podman/v4/pkg/rootless" "github.com/containers/podman/v4/pkg/specgen" + "github.com/hashicorp/go-multierror" "github.com/sirupsen/logrus" ) @@ -191,29 +192,9 @@ func (r *Runtime) SavePod(pod *Pod) error { return nil } -func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool, timeout *uint) error { - if err := p.updatePod(); err != nil { - return err - } - - ctrs, err := r.state.PodContainers(p) - if err != nil { - return err - } - numCtrs := len(ctrs) - - // If the only running container in the pod is the pause container, remove the pod and container unconditionally. - pauseCtrID := p.state.InfraContainerID - if numCtrs == 1 && ctrs[0].ID() == pauseCtrID { - removeCtrs = true - force = true - } - if !removeCtrs && numCtrs > 0 { - return fmt.Errorf("pod %s contains containers and cannot be removed: %w", p.ID(), define.ErrCtrExists) - } - - ctrNamedVolumes := make(map[string]*ContainerNamedVolume) - +// DO NOT USE THIS FUNCTION DIRECTLY. Use removePod(), below. It will call +// removeMalformedPod() if necessary. +func (r *Runtime) removeMalformedPod(ctx context.Context, p *Pod, ctrs []*Container, force bool, timeout *uint, ctrNamedVolumes map[string]*ContainerNamedVolume) error { var removalErr error for _, ctr := range ctrs { err := func() error { @@ -231,7 +212,7 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool, ctrNamedVolumes[vol.Name] = vol } - return r.removeContainer(ctx, ctr, force, false, true, timeout) + return r.removeContainer(ctx, ctr, force, false, true, true, timeout) }() if removalErr == nil { @@ -261,6 +242,69 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool, return err } + return nil +} + +func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool, timeout *uint) error { + if err := p.updatePod(); err != nil { + return err + } + + ctrs, err := r.state.PodContainers(p) + if err != nil { + return err + } + numCtrs := len(ctrs) + + // If the only running container in the pod is the pause container, remove the pod and container unconditionally. + pauseCtrID := p.state.InfraContainerID + if numCtrs == 1 && ctrs[0].ID() == pauseCtrID { + removeCtrs = true + force = true + } + if !removeCtrs && numCtrs > 0 { + return fmt.Errorf("pod %s contains containers and cannot be removed: %w", p.ID(), define.ErrCtrExists) + } + + var removalErr error + ctrNamedVolumes := make(map[string]*ContainerNamedVolume) + + // Build a graph of all containers in the pod. + graph, err := BuildContainerGraph(ctrs) + if err != nil { + // We have to allow the pod to be removed. + // But let's only do it if force is set. + if !force { + return fmt.Errorf("cannot create container graph for pod %s: %w", p.ID(), err) + } + + removalErr = fmt.Errorf("creating container graph for pod %s failed, fell back to loop removal: %w", p.ID(), err) + + if err := r.removeMalformedPod(ctx, p, ctrs, force, timeout, ctrNamedVolumes); err != nil { + logrus.Errorf("Error creating container graph for pod %s: %v. Falling back to loop removal.", p.ID(), err) + return err + } + } else { + ctrErrors := make(map[string]error) + ctrsVisited := make(map[string]bool) + + for _, node := range graph.notDependedOnNodes { + removeNode(ctx, node, p, force, timeout, false, ctrErrors, ctrsVisited, ctrNamedVolumes) + } + + // This is gross, but I don't want to change the signature on + // removePod - especially since any change here eventually has + // to map down to one error unless we want to make a breaking + // API change. + if len(ctrErrors) > 0 { + var allErrs error + for id, err := range ctrErrors { + allErrs = multierror.Append(allErrs, fmt.Errorf("removing container %s from pod %s: %w", id, p.ID(), err)) + } + return allErrs + } + } + for volName := range ctrNamedVolumes { volume, err := r.state.Volume(volName) if err != nil && !errors.Is(err, define.ErrNoSuchVolume) { diff --git a/libpod/runtime_volume_linux.go b/libpod/runtime_volume_linux.go index c9a4a7dc1..c59417979 100644 --- a/libpod/runtime_volume_linux.go +++ b/libpod/runtime_volume_linux.go @@ -15,6 +15,7 @@ import ( "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/libpod/events" volplugin "github.com/containers/podman/v4/libpod/plugin" + "github.com/containers/storage" "github.com/containers/storage/drivers/quota" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/stringid" @@ -22,18 +23,20 @@ import ( "github.com/sirupsen/logrus" ) +const volumeSuffix = "+volume" + // NewVolume creates a new empty volume func (r *Runtime) NewVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) { if !r.valid { return nil, define.ErrRuntimeStopped } - return r.newVolume(false, options...) + return r.newVolume(ctx, false, options...) } // newVolume creates a new empty volume with the given options. // The createPluginVolume can be set to true to make it not create the volume in the volume plugin, // this is required for the UpdateVolumePlugins() function. If you are not sure, set this to false. -func (r *Runtime) newVolume(noCreatePluginVolume bool, options ...VolumeCreateOption) (_ *Volume, deferredErr error) { +func (r *Runtime) newVolume(ctx context.Context, noCreatePluginVolume bool, options ...VolumeCreateOption) (_ *Volume, deferredErr error) { volume := newVolume(r) for _, option := range options { if err := option(volume); err != nil { @@ -83,6 +86,50 @@ func (r *Runtime) newVolume(noCreatePluginVolume bool, options ...VolumeCreateOp return nil, fmt.Errorf("invalid mount option %s for driver 'local': %w", key, define.ErrInvalidArg) } } + } else if volume.config.Driver == define.VolumeDriverImage && !volume.UsesVolumeDriver() { + logrus.Debugf("Creating image-based volume") + var imgString string + // Validate options + for key, val := range volume.config.Options { + switch strings.ToLower(key) { + case "image": + imgString = val + default: + return nil, fmt.Errorf("invalid mount option %s for driver 'image': %w", key, define.ErrInvalidArg) + } + } + + if imgString == "" { + return nil, fmt.Errorf("must provide an image name when creating a volume with the image driver: %w", define.ErrInvalidArg) + } + + // Look up the image + image, _, err := r.libimageRuntime.LookupImage(imgString, nil) + if err != nil { + return nil, fmt.Errorf("looking up image %s to create volume failed: %w", imgString, err) + } + + // Generate a c/storage name and ID for the volume. + // Use characters Podman does not allow for the name, to ensure + // no collision with containers. + volume.config.StorageID = stringid.GenerateRandomID() + volume.config.StorageName = volume.config.Name + volumeSuffix + volume.config.StorageImageID = image.ID() + + // Create a backing container in c/storage. + storageConfig := storage.ContainerOptions{ + LabelOpts: []string{"filetype:container_file_t:s0"}, + } + if _, err := r.storageService.CreateContainerStorage(ctx, r.imageContext, imgString, image.ID(), volume.config.StorageName, volume.config.StorageID, storageConfig); err != nil { + return nil, fmt.Errorf("creating backing storage for image driver: %w", err) + } + defer func() { + if deferredErr != nil { + if err := r.storageService.DeleteContainer(volume.config.StorageID); err != nil { + logrus.Errorf("Error removing volume %s backing storage: %v", volume.config.Name, err) + } + } + }() } // Now we get conditional: we either need to make the volume in the @@ -196,7 +243,7 @@ func (r *Runtime) UpdateVolumePlugins(ctx context.Context) *define.VolumeReload } for _, vol := range vols { allPluginVolumes[vol.Name] = struct{}{} - if _, err := r.newVolume(true, WithVolumeName(vol.Name), WithVolumeDriver(driverName)); err != nil { + if _, err := r.newVolume(ctx, true, WithVolumeName(vol.Name), WithVolumeDriver(driverName)); err != nil { // If the volume exists this is not an error, just ignore it and log. It is very likely // that the volume from the plugin was already in our db. if !errors.Is(err, define.ErrVolumeExists) { @@ -324,7 +371,7 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo logrus.Debugf("Removing container %s (depends on volume %q)", ctr.ID(), v.Name()) - if err := r.removeContainer(ctx, ctr, force, false, false, timeout); err != nil { + if err := r.removeContainer(ctx, ctr, force, false, false, false, timeout); err != nil { return fmt.Errorf("removing container %s that depends on volume %s: %w", ctr.ID(), v.Name(), err) } } @@ -375,6 +422,14 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo return fmt.Errorf("volume %s could not be removed from plugin %s: %w", v.Name(), v.Driver(), err) } } + } else if v.config.Driver == define.VolumeDriverImage { + if err := v.runtime.storageService.DeleteContainer(v.config.StorageID); err != nil { + // Storage container is already gone, no problem. + if !(errors.Is(err, storage.ErrNotAContainer) || errors.Is(err, storage.ErrContainerUnknown)) { + return fmt.Errorf("removing volume %s storage: %w", v.Name(), err) + } + logrus.Infof("Storage for volume %s already removed", v.Name()) + } } // Remove the volume from the state diff --git a/libpod/state.go b/libpod/state.go index 4fbd3c302..9d9604563 100644 --- a/libpod/state.go +++ b/libpod/state.go @@ -144,6 +144,14 @@ type State interface { // As with RemoveExecSession, container state will not be modified. RemoveContainerExecSessions(ctr *Container) error + // ContainerIDIsVolume checks if the given container ID is in use by a + // volume. + // Some volumes are backed by a c/storage container. These do not have a + // corresponding Container struct in Libpod, but rather a Volume. + // This determines if a given ID from c/storage is used as a backend by + // a Podman volume. + ContainerIDIsVolume(id string) (bool, error) + // PLEASE READ FULL DESCRIPTION BEFORE USING. // Rewrite a container's configuration. // This function breaks libpod's normal prohibition on a read-only diff --git a/libpod/state_test.go b/libpod/state_test.go index 3c1fe8f63..7664f7c00 100644 --- a/libpod/state_test.go +++ b/libpod/state_test.go @@ -1,7 +1,6 @@ package libpod import ( - "io/ioutil" "os" "path/filepath" "strings" @@ -35,7 +34,7 @@ var ( // Get an empty BoltDB state for use in tests func getEmptyBoltState() (_ State, _ string, _ lock.Manager, retErr error) { - tmpDir, err := ioutil.TempDir("", tmpDirPrefix) + tmpDir, err := os.MkdirTemp("", tmpDirPrefix) if err != nil { return nil, "", nil, err } diff --git a/libpod/volume.go b/libpod/volume.go index a054e4032..2d4ea4280 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -57,6 +57,15 @@ type VolumeConfig struct { DisableQuota bool `json:"disableQuota,omitempty"` // Timeout allows users to override the default driver timeout of 5 seconds Timeout *uint `json:"timeout,omitempty"` + // StorageName is the name of the volume in c/storage. Only used for + // image volumes. + StorageName string `json:"storageName,omitempty"` + // StorageID is the ID of the volume in c/storage. Only used for image + // volumes. + StorageID string `json:"storageID,omitempty"` + // StorageImageID is the ID of the image the volume was based off of. + // Only used for image volumes. + StorageImageID string `json:"storageImageID,omitempty"` } // VolumeState holds the volume's mutable state. @@ -149,7 +158,7 @@ func (v *Volume) MountCount() (uint, error) { // Internal-only helper for volume mountpoint func (v *Volume) mountPoint() string { - if v.UsesVolumeDriver() { + if v.UsesVolumeDriver() || v.config.Driver == define.VolumeDriverImage { return v.state.MountPoint } @@ -250,6 +259,12 @@ func (v *Volume) IsDangling() (bool, error) { // drivers are pluggable backends for volumes that will manage the storage and // mounting. func (v *Volume) UsesVolumeDriver() bool { + if v.config.Driver == define.VolumeDriverImage { + if _, ok := v.runtime.config.Engine.VolumePlugins[v.config.Driver]; ok { + return true + } + return false + } return !(v.config.Driver == define.VolumeDriverLocal || v.config.Driver == "") } diff --git a/libpod/volume_inspect.go b/libpod/volume_inspect.go index 73441576b..31fbd5eff 100644 --- a/libpod/volume_inspect.go +++ b/libpod/volume_inspect.go @@ -64,6 +64,7 @@ func (v *Volume) Inspect() (*define.InspectVolumeData, error) { data.MountCount = v.state.MountCount data.NeedsCopyUp = v.state.NeedsCopyUp data.NeedsChown = v.state.NeedsChown + data.StorageID = v.config.StorageID if v.config.Timeout != nil { data.Timeout = *v.config.Timeout diff --git a/libpod/volume_internal.go b/libpod/volume_internal.go index 43c3f9b0b..14b852f8e 100644 --- a/libpod/volume_internal.go +++ b/libpod/volume_internal.go @@ -39,6 +39,11 @@ func (v *Volume) needsMount() bool { return true } + // Image driver always needs mount + if v.config.Driver == define.VolumeDriverImage { + return true + } + // Commit 28138dafcc added the UID and GID options to this map // However we should only mount when options other than uid and gid are set. // see https://github.com/containers/podman/issues/10620 diff --git a/libpod/volume_internal_linux.go b/libpod/volume_internal_linux.go index cfd60554d..440bceec3 100644 --- a/libpod/volume_internal_linux.go +++ b/libpod/volume_internal_linux.go @@ -66,6 +66,15 @@ func (v *Volume) mount() error { v.state.MountCount++ v.state.MountPoint = mountPoint return v.save() + } else if v.config.Driver == define.VolumeDriverImage { + mountPoint, err := v.runtime.storageService.MountContainerImage(v.config.StorageID) + if err != nil { + return fmt.Errorf("mounting volume %s image failed: %w", v.Name(), err) + } + + v.state.MountCount++ + v.state.MountPoint = mountPoint + return v.save() } volDevice := v.config.Options["device"] @@ -161,6 +170,13 @@ func (v *Volume) unmount(force bool) error { v.state.MountPoint = "" return v.save() + } else if v.config.Driver == define.VolumeDriverImage { + if _, err := v.runtime.storageService.UnmountContainerImage(v.config.StorageID, force); err != nil { + return fmt.Errorf("unmounting volume %s image: %w", v.Name(), err) + } + + v.state.MountPoint = "" + return v.save() } // Unmount the volume |