mirror of
https://github.com/containers/skopeo.git
synced 2025-04-27 11:01:18 +00:00
621 lines
18 KiB
Go
621 lines
18 KiB
Go
package idtools
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/user"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"github.com/containers/storage/pkg/system"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// IDMap contains a single entry for user namespace range remapping. An array
|
|
// of IDMap entries represents the structure that will be provided to the Linux
|
|
// kernel for creating a user namespace.
|
|
type IDMap struct {
|
|
ContainerID int `json:"container_id"`
|
|
HostID int `json:"host_id"`
|
|
Size int `json:"size"`
|
|
}
|
|
|
|
type subIDRange struct {
|
|
Start int
|
|
Length int
|
|
}
|
|
|
|
type ranges []subIDRange
|
|
|
|
func (e ranges) Len() int { return len(e) }
|
|
func (e ranges) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
|
|
func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start }
|
|
|
|
const (
|
|
subuidFileName string = "/etc/subuid"
|
|
subgidFileName string = "/etc/subgid"
|
|
ContainersOverrideXattr = "user.containers.override_stat"
|
|
)
|
|
|
|
// MkdirAllAs creates a directory (include any along the path) and then modifies
|
|
// ownership to the requested uid/gid. If the directory already exists, this
|
|
// function will still change ownership to the requested uid/gid pair.
|
|
// Deprecated: Use MkdirAllAndChown
|
|
func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
|
|
return mkdirAs(path, mode, ownerUID, ownerGID, true, true)
|
|
}
|
|
|
|
// MkdirAs creates a directory and then modifies ownership to the requested uid/gid.
|
|
// If the directory already exists, this function still changes ownership
|
|
// Deprecated: Use MkdirAndChown with a IDPair
|
|
func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
|
|
return mkdirAs(path, mode, ownerUID, ownerGID, false, true)
|
|
}
|
|
|
|
// MkdirAllAndChown creates a directory (include any along the path) and then modifies
|
|
// ownership to the requested uid/gid. If the directory already exists, this
|
|
// function will still change ownership to the requested uid/gid pair.
|
|
func MkdirAllAndChown(path string, mode os.FileMode, ids IDPair) error {
|
|
return mkdirAs(path, mode, ids.UID, ids.GID, true, true)
|
|
}
|
|
|
|
// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid.
|
|
// If the directory already exists, this function still changes ownership
|
|
func MkdirAndChown(path string, mode os.FileMode, ids IDPair) error {
|
|
return mkdirAs(path, mode, ids.UID, ids.GID, false, true)
|
|
}
|
|
|
|
// MkdirAllAndChownNew creates a directory (include any along the path) and then modifies
|
|
// ownership ONLY of newly created directories to the requested uid/gid. If the
|
|
// directories along the path exist, no change of ownership will be performed
|
|
func MkdirAllAndChownNew(path string, mode os.FileMode, ids IDPair) error {
|
|
return mkdirAs(path, mode, ids.UID, ids.GID, true, false)
|
|
}
|
|
|
|
// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
|
|
// If the maps are empty, then the root uid/gid will default to "real" 0/0
|
|
func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
|
|
var uid, gid int
|
|
var err error
|
|
if len(uidMap) == 1 && uidMap[0].Size == 1 {
|
|
uid = uidMap[0].HostID
|
|
} else {
|
|
uid, err = RawToHost(0, uidMap)
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
}
|
|
if len(gidMap) == 1 && gidMap[0].Size == 1 {
|
|
gid = gidMap[0].HostID
|
|
} else {
|
|
gid, err = RawToHost(0, gidMap)
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
}
|
|
return uid, gid, nil
|
|
}
|
|
|
|
// RawToContainer takes an id mapping, and uses it to translate a host ID to
|
|
// the remapped ID. If no map is provided, then the translation assumes a
|
|
// 1-to-1 mapping and returns the passed in id.
|
|
//
|
|
// If you wish to map a (uid,gid) combination you should use the corresponding
|
|
// IDMappings methods, which ensure that you are mapping the correct ID against
|
|
// the correct mapping.
|
|
func RawToContainer(hostID int, idMap []IDMap) (int, error) {
|
|
if idMap == nil {
|
|
return hostID, nil
|
|
}
|
|
for _, m := range idMap {
|
|
if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) {
|
|
contID := m.ContainerID + (hostID - m.HostID)
|
|
return contID, nil
|
|
}
|
|
}
|
|
return -1, fmt.Errorf("host ID %d cannot be mapped to a container ID", hostID)
|
|
}
|
|
|
|
// RawToHost takes an id mapping and a remapped ID, and translates the ID to
|
|
// the mapped host ID. If no map is provided, then the translation assumes a
|
|
// 1-to-1 mapping and returns the passed in id.
|
|
//
|
|
// If you wish to map a (uid,gid) combination you should use the corresponding
|
|
// IDMappings methods, which ensure that you are mapping the correct ID against
|
|
// the correct mapping.
|
|
func RawToHost(contID int, idMap []IDMap) (int, error) {
|
|
if idMap == nil {
|
|
return contID, nil
|
|
}
|
|
for _, m := range idMap {
|
|
if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) {
|
|
hostID := m.HostID + (contID - m.ContainerID)
|
|
return hostID, nil
|
|
}
|
|
}
|
|
return -1, fmt.Errorf("container ID %d cannot be mapped to a host ID", contID)
|
|
}
|
|
|
|
// IDPair is a UID and GID pair
|
|
type IDPair struct {
|
|
UID int
|
|
GID int
|
|
}
|
|
|
|
// IDMappings contains a mappings of UIDs and GIDs
|
|
type IDMappings struct {
|
|
uids []IDMap
|
|
gids []IDMap
|
|
}
|
|
|
|
// NewIDMappings takes a requested user and group name and
|
|
// using the data from /etc/sub{uid,gid} ranges, creates the
|
|
// proper uid and gid remapping ranges for that user/group pair
|
|
func NewIDMappings(username, groupname string) (*IDMappings, error) {
|
|
subuidRanges, err := readSubuid(username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
subgidRanges, err := readSubgid(groupname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(subuidRanges) == 0 {
|
|
return nil, fmt.Errorf("no subuid ranges found for user %q in %s", username, subuidFileName)
|
|
}
|
|
if len(subgidRanges) == 0 {
|
|
return nil, fmt.Errorf("no subgid ranges found for group %q in %s", groupname, subgidFileName)
|
|
}
|
|
|
|
return &IDMappings{
|
|
uids: createIDMap(subuidRanges),
|
|
gids: createIDMap(subgidRanges),
|
|
}, nil
|
|
}
|
|
|
|
// NewIDMappingsFromMaps creates a new mapping from two slices
|
|
// Deprecated: this is a temporary shim while transitioning to IDMapping
|
|
func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IDMappings {
|
|
return &IDMappings{uids: uids, gids: gids}
|
|
}
|
|
|
|
// RootPair returns a uid and gid pair for the root user. The error is ignored
|
|
// because a root user always exists, and the defaults are correct when the uid
|
|
// and gid maps are empty.
|
|
func (i *IDMappings) RootPair() IDPair {
|
|
uid, gid, _ := GetRootUIDGID(i.uids, i.gids)
|
|
return IDPair{UID: uid, GID: gid}
|
|
}
|
|
|
|
// ToHost returns the host UID and GID for the container uid, gid.
|
|
func (i *IDMappings) ToHost(pair IDPair) (IDPair, error) {
|
|
var err error
|
|
var target IDPair
|
|
|
|
target.UID, err = RawToHost(pair.UID, i.uids)
|
|
if err != nil {
|
|
return target, err
|
|
}
|
|
|
|
target.GID, err = RawToHost(pair.GID, i.gids)
|
|
return target, err
|
|
}
|
|
|
|
var (
|
|
overflowUIDOnce sync.Once
|
|
overflowGIDOnce sync.Once
|
|
overflowUID int
|
|
overflowGID int
|
|
)
|
|
|
|
// getOverflowUID returns the UID mapped to the overflow user
|
|
func getOverflowUID() int {
|
|
overflowUIDOnce.Do(func() {
|
|
// 65534 is the value on older kernels where /proc/sys/kernel/overflowuid is not present
|
|
overflowUID = 65534
|
|
if content, err := os.ReadFile("/proc/sys/kernel/overflowuid"); err == nil {
|
|
if tmp, err := strconv.Atoi(string(content)); err == nil {
|
|
overflowUID = tmp
|
|
}
|
|
}
|
|
})
|
|
return overflowUID
|
|
}
|
|
|
|
// getOverflowGID returns the GID mapped to the overflow user
|
|
func getOverflowGID() int {
|
|
overflowGIDOnce.Do(func() {
|
|
// 65534 is the value on older kernels where /proc/sys/kernel/overflowgid is not present
|
|
overflowGID = 65534
|
|
if content, err := os.ReadFile("/proc/sys/kernel/overflowgid"); err == nil {
|
|
if tmp, err := strconv.Atoi(string(content)); err == nil {
|
|
overflowGID = tmp
|
|
}
|
|
}
|
|
})
|
|
return overflowGID
|
|
}
|
|
|
|
// ToHost returns the host UID and GID for the container uid, gid.
|
|
// Remapping is only performed if the ids aren't already the remapped root ids
|
|
// If the mapping is not possible because the target ID is not mapped into
|
|
// the namespace, then the overflow ID is used.
|
|
func (i *IDMappings) ToHostOverflow(pair IDPair) (IDPair, error) {
|
|
var err error
|
|
target := i.RootPair()
|
|
|
|
if pair.UID != target.UID {
|
|
target.UID, err = RawToHost(pair.UID, i.uids)
|
|
if err != nil {
|
|
target.UID = getOverflowUID()
|
|
logrus.Debugf("Failed to map UID %v to the target mapping, using the overflow ID %v", pair.UID, target.UID)
|
|
}
|
|
}
|
|
|
|
if pair.GID != target.GID {
|
|
target.GID, err = RawToHost(pair.GID, i.gids)
|
|
if err != nil {
|
|
target.GID = getOverflowGID()
|
|
logrus.Debugf("Failed to map GID %v to the target mapping, using the overflow ID %v", pair.GID, target.GID)
|
|
}
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
// ToContainer returns the container UID and GID for the host uid and gid
|
|
func (i *IDMappings) ToContainer(pair IDPair) (int, int, error) {
|
|
uid, err := RawToContainer(pair.UID, i.uids)
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
gid, err := RawToContainer(pair.GID, i.gids)
|
|
return uid, gid, err
|
|
}
|
|
|
|
// Empty returns true if there are no id mappings
|
|
func (i *IDMappings) Empty() bool {
|
|
return len(i.uids) == 0 && len(i.gids) == 0
|
|
}
|
|
|
|
// UIDs return the UID mapping
|
|
// TODO: remove this once everything has been refactored to use pairs
|
|
func (i *IDMappings) UIDs() []IDMap {
|
|
return i.uids
|
|
}
|
|
|
|
// GIDs return the UID mapping
|
|
// TODO: remove this once everything has been refactored to use pairs
|
|
func (i *IDMappings) GIDs() []IDMap {
|
|
return i.gids
|
|
}
|
|
|
|
func createIDMap(subidRanges ranges) []IDMap {
|
|
idMap := []IDMap{}
|
|
|
|
// sort the ranges by lowest ID first
|
|
sort.Sort(subidRanges)
|
|
containerID := 0
|
|
for _, idrange := range subidRanges {
|
|
idMap = append(idMap, IDMap{
|
|
ContainerID: containerID,
|
|
HostID: idrange.Start,
|
|
Size: idrange.Length,
|
|
})
|
|
containerID = containerID + idrange.Length
|
|
}
|
|
return idMap
|
|
}
|
|
|
|
// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid)
|
|
// and return all found ranges for a specified username. If the special value
|
|
// "ALL" is supplied for username, then all ranges in the file will be returned
|
|
func parseSubidFile(path, username string) (ranges, error) {
|
|
var (
|
|
rangeList ranges
|
|
uidstr string
|
|
)
|
|
if u, err := user.Lookup(username); err == nil {
|
|
uidstr = u.Uid
|
|
}
|
|
|
|
subidFile, err := os.Open(path)
|
|
if err != nil {
|
|
return rangeList, err
|
|
}
|
|
defer subidFile.Close()
|
|
|
|
s := bufio.NewScanner(subidFile)
|
|
for s.Scan() {
|
|
if err := s.Err(); err != nil {
|
|
return rangeList, err
|
|
}
|
|
|
|
text := strings.TrimSpace(s.Text())
|
|
if text == "" || strings.HasPrefix(text, "#") {
|
|
continue
|
|
}
|
|
parts := strings.Split(text, ":")
|
|
if len(parts) != 3 {
|
|
return rangeList, fmt.Errorf("cannot parse subuid/gid information: Format not correct for %s file", path)
|
|
}
|
|
if parts[0] == username || username == "ALL" || (parts[0] == uidstr && parts[0] != "") {
|
|
startid, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return rangeList, fmt.Errorf("string to int conversion failed during subuid/gid parsing of %s: %w", path, err)
|
|
}
|
|
length, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return rangeList, fmt.Errorf("string to int conversion failed during subuid/gid parsing of %s: %w", path, err)
|
|
}
|
|
rangeList = append(rangeList, subIDRange{startid, length})
|
|
}
|
|
}
|
|
return rangeList, nil
|
|
}
|
|
|
|
func checkChownErr(err error, name string, uid, gid int) error {
|
|
var e *os.PathError
|
|
if errors.As(err, &e) && e.Err == syscall.EINVAL {
|
|
return fmt.Errorf(`potentially insufficient UIDs or GIDs available in user namespace (requested %d:%d for %s): Check /etc/subuid and /etc/subgid if configured locally and run "podman system migrate": %w`, uid, gid, name, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Stat contains file states that can be overridden with ContainersOverrideXattr.
|
|
type Stat struct {
|
|
IDs IDPair
|
|
Mode os.FileMode
|
|
Major int
|
|
Minor int
|
|
}
|
|
|
|
// FormatContainersOverrideXattr will format the given uid, gid, and mode into a string
|
|
// that can be used as the value for the ContainersOverrideXattr xattr.
|
|
func FormatContainersOverrideXattr(uid, gid, mode int) string {
|
|
return FormatContainersOverrideXattrDevice(uid, gid, fs.FileMode(mode), 0, 0)
|
|
}
|
|
|
|
// FormatContainersOverrideXattrDevice will format the given uid, gid, and mode into a string
|
|
// that can be used as the value for the ContainersOverrideXattr xattr. For devices, it also
|
|
// needs the major and minor numbers.
|
|
func FormatContainersOverrideXattrDevice(uid, gid int, mode fs.FileMode, major, minor int) string {
|
|
typ := ""
|
|
switch mode & os.ModeType {
|
|
case os.ModeDir:
|
|
typ = "dir"
|
|
case os.ModeSymlink:
|
|
typ = "symlink"
|
|
case os.ModeNamedPipe:
|
|
typ = "pipe"
|
|
case os.ModeSocket:
|
|
typ = "socket"
|
|
case os.ModeDevice:
|
|
typ = fmt.Sprintf("block-%d-%d", major, minor)
|
|
case os.ModeDevice | os.ModeCharDevice:
|
|
typ = fmt.Sprintf("char-%d-%d", major, minor)
|
|
default:
|
|
typ = "file"
|
|
}
|
|
unixMode := mode & os.ModePerm
|
|
if mode&os.ModeSetuid != 0 {
|
|
unixMode |= 0o4000
|
|
}
|
|
if mode&os.ModeSetgid != 0 {
|
|
unixMode |= 0o2000
|
|
}
|
|
if mode&os.ModeSticky != 0 {
|
|
unixMode |= 0o1000
|
|
}
|
|
return fmt.Sprintf("%d:%d:%04o:%s", uid, gid, unixMode, typ)
|
|
}
|
|
|
|
// GetContainersOverrideXattr will get and decode ContainersOverrideXattr.
|
|
func GetContainersOverrideXattr(path string) (Stat, error) {
|
|
xstat, err := system.Lgetxattr(path, ContainersOverrideXattr)
|
|
if err != nil {
|
|
return Stat{}, err
|
|
}
|
|
return parseOverrideXattr(xstat) // This will fail if (xstat, err) == (nil, nil), i.e. the xattr does not exist.
|
|
}
|
|
|
|
func parseOverrideXattr(xstat []byte) (Stat, error) {
|
|
var stat Stat
|
|
attrs := strings.Split(string(xstat), ":")
|
|
if len(attrs) < 3 {
|
|
return stat, fmt.Errorf("the number of parts in %s is less than 3",
|
|
ContainersOverrideXattr)
|
|
}
|
|
|
|
value, err := strconv.ParseUint(attrs[0], 10, 32)
|
|
if err != nil {
|
|
return stat, fmt.Errorf("failed to parse UID: %w", err)
|
|
}
|
|
stat.IDs.UID = int(value)
|
|
|
|
value, err = strconv.ParseUint(attrs[1], 10, 32)
|
|
if err != nil {
|
|
return stat, fmt.Errorf("failed to parse GID: %w", err)
|
|
}
|
|
stat.IDs.GID = int(value)
|
|
|
|
value, err = strconv.ParseUint(attrs[2], 8, 32)
|
|
if err != nil {
|
|
return stat, fmt.Errorf("failed to parse mode: %w", err)
|
|
}
|
|
stat.Mode = os.FileMode(value) & os.ModePerm
|
|
if value&0o1000 != 0 {
|
|
stat.Mode |= os.ModeSticky
|
|
}
|
|
if value&0o2000 != 0 {
|
|
stat.Mode |= os.ModeSetgid
|
|
}
|
|
if value&0o4000 != 0 {
|
|
stat.Mode |= os.ModeSetuid
|
|
}
|
|
|
|
if len(attrs) > 3 {
|
|
typ := attrs[3]
|
|
if strings.HasPrefix(typ, "file") {
|
|
} else if strings.HasPrefix(typ, "dir") {
|
|
stat.Mode |= os.ModeDir
|
|
} else if strings.HasPrefix(typ, "symlink") {
|
|
stat.Mode |= os.ModeSymlink
|
|
} else if strings.HasPrefix(typ, "pipe") {
|
|
stat.Mode |= os.ModeNamedPipe
|
|
} else if strings.HasPrefix(typ, "socket") {
|
|
stat.Mode |= os.ModeSocket
|
|
} else if strings.HasPrefix(typ, "block") {
|
|
stat.Mode |= os.ModeDevice
|
|
stat.Major, stat.Minor, err = parseDevice(typ)
|
|
if err != nil {
|
|
return stat, err
|
|
}
|
|
} else if strings.HasPrefix(typ, "char") {
|
|
stat.Mode |= os.ModeDevice | os.ModeCharDevice
|
|
stat.Major, stat.Minor, err = parseDevice(typ)
|
|
if err != nil {
|
|
return stat, err
|
|
}
|
|
} else {
|
|
return stat, fmt.Errorf("invalid file type %s", typ)
|
|
}
|
|
}
|
|
return stat, nil
|
|
}
|
|
|
|
func parseDevice(typ string) (int, int, error) {
|
|
parts := strings.Split(typ, "-")
|
|
// If there are more than 3 parts, just ignore them to be forward compatible
|
|
if len(parts) < 3 {
|
|
return 0, 0, fmt.Errorf("invalid device type %s", typ)
|
|
}
|
|
if parts[0] != "block" && parts[0] != "char" {
|
|
return 0, 0, fmt.Errorf("invalid device type %s", typ)
|
|
}
|
|
major, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("failed to parse major number: %w", err)
|
|
}
|
|
minor, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("failed to parse minor number: %w", err)
|
|
}
|
|
return major, minor, nil
|
|
}
|
|
|
|
// SetContainersOverrideXattr will encode and set ContainersOverrideXattr.
|
|
func SetContainersOverrideXattr(path string, stat Stat) error {
|
|
value := FormatContainersOverrideXattrDevice(stat.IDs.UID, stat.IDs.GID, stat.Mode, stat.Major, stat.Minor)
|
|
return system.Lsetxattr(path, ContainersOverrideXattr, []byte(value), 0)
|
|
}
|
|
|
|
func SafeChown(name string, uid, gid int) error {
|
|
if runtime.GOOS == "darwin" {
|
|
stat := Stat{
|
|
Mode: os.FileMode(0o0700),
|
|
}
|
|
xstat, err := system.Lgetxattr(name, ContainersOverrideXattr)
|
|
if err == nil && xstat != nil {
|
|
stat, err = parseOverrideXattr(xstat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
st, err := os.Stat(name) // Ideally we would share this with system.Stat below, but then we would need to convert Mode.
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stat.Mode = st.Mode()
|
|
}
|
|
stat.IDs = IDPair{UID: uid, GID: gid}
|
|
if err = SetContainersOverrideXattr(name, stat); err != nil {
|
|
return err
|
|
}
|
|
uid = os.Getuid()
|
|
gid = os.Getgid()
|
|
}
|
|
if stat, statErr := system.Stat(name); statErr == nil {
|
|
if stat.UID() == uint32(uid) && stat.GID() == uint32(gid) {
|
|
return nil
|
|
}
|
|
}
|
|
return checkChownErr(os.Chown(name, uid, gid), name, uid, gid)
|
|
}
|
|
|
|
func SafeLchown(name string, uid, gid int) error {
|
|
if runtime.GOOS == "darwin" {
|
|
stat := Stat{
|
|
Mode: os.FileMode(0o0700),
|
|
}
|
|
xstat, err := system.Lgetxattr(name, ContainersOverrideXattr)
|
|
if err == nil && xstat != nil {
|
|
stat, err = parseOverrideXattr(xstat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
st, err := os.Lstat(name) // Ideally we would share this with system.Stat below, but then we would need to convert Mode.
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stat.Mode = st.Mode()
|
|
}
|
|
stat.IDs = IDPair{UID: uid, GID: gid}
|
|
if err = SetContainersOverrideXattr(name, stat); err != nil {
|
|
return err
|
|
}
|
|
uid = os.Getuid()
|
|
gid = os.Getgid()
|
|
}
|
|
if stat, statErr := system.Lstat(name); statErr == nil {
|
|
if stat.UID() == uint32(uid) && stat.GID() == uint32(gid) {
|
|
return nil
|
|
}
|
|
}
|
|
return checkChownErr(os.Lchown(name, uid, gid), name, uid, gid)
|
|
}
|
|
|
|
type sortByHostID []IDMap
|
|
|
|
func (e sortByHostID) Len() int { return len(e) }
|
|
func (e sortByHostID) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
|
|
func (e sortByHostID) Less(i, j int) bool { return e[i].HostID < e[j].HostID }
|
|
|
|
type sortByContainerID []IDMap
|
|
|
|
func (e sortByContainerID) Len() int { return len(e) }
|
|
func (e sortByContainerID) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
|
|
func (e sortByContainerID) Less(i, j int) bool { return e[i].ContainerID < e[j].ContainerID }
|
|
|
|
// IsContiguous checks if the specified mapping is contiguous and doesn't
|
|
// have any hole.
|
|
func IsContiguous(mappings []IDMap) bool {
|
|
if len(mappings) < 2 {
|
|
return true
|
|
}
|
|
|
|
var mh sortByHostID = mappings[:]
|
|
sort.Sort(mh)
|
|
for i := 1; i < len(mh); i++ {
|
|
if mh[i].HostID != mh[i-1].HostID+mh[i-1].Size {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var mc sortByContainerID = mappings[:]
|
|
sort.Sort(mc)
|
|
for i := 1; i < len(mc); i++ {
|
|
if mc[i].ContainerID != mc[i-1].ContainerID+mc[i-1].Size {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|