浏览代码

Merge pull request #9023 from cyphar/libcontainer-update-to-new-interface

Update vendor'd `libcontainer` and update to use new `user` interface.
unclejack 10 年之前
父节点
当前提交
b18efeeb7a

+ 5 - 1
api/server/server.go

@@ -1370,7 +1370,11 @@ func ServeFd(addr string, handle http.Handler) error {
 }
 
 func lookupGidByName(nameOrGid string) (int, error) {
-	groups, err := user.ParseGroupFilter(func(g *user.Group) bool {
+	groupFile, err := user.GetGroupFile()
+	if err != nil {
+		return -1, err
+	}
+	groups, err := user.ParseGroupFileFilter(groupFile, func(g user.Group) bool {
 		return g.Name == nameOrGid || strconv.Itoa(g.Gid) == nameOrGid
 	})
 	if err != nil {

+ 1 - 1
hack/vendor.sh

@@ -66,7 +66,7 @@ if [ "$1" = '--go' ]; then
 	mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar
 fi
 
-clone git github.com/docker/libcontainer fd6df76562137aa3b18e44b790cb484fe2b6fa0b
+clone git github.com/docker/libcontainer 4ae31b6ceb2c2557c9f05f42da61b0b808faa5a4
 # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file)
 rm -rf src/github.com/docker/libcontainer/vendor
 eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')"

+ 22 - 5
vendor/src/github.com/docker/libcontainer/namespaces/init.go

@@ -167,26 +167,43 @@ func RestoreParentDeathSignal(old int) error {
 
 // SetupUser changes the groups, gid, and uid for the user inside the container
 func SetupUser(u string) error {
-	uid, gid, suppGids, home, err := user.GetUserGroupSupplementaryHome(u, syscall.Getuid(), syscall.Getgid(), "/")
+	// Set up defaults.
+	defaultExecUser := user.ExecUser{
+		Uid:  syscall.Getuid(),
+		Gid:  syscall.Getgid(),
+		Home: "/",
+	}
+
+	passwdFile, err := user.GetPasswdFile()
+	if err != nil {
+		return err
+	}
+
+	groupFile, err := user.GetGroupFile()
+	if err != nil {
+		return err
+	}
+
+	execUser, err := user.GetExecUserFile(u, &defaultExecUser, passwdFile, groupFile)
 	if err != nil {
 		return fmt.Errorf("get supplementary groups %s", err)
 	}
 
-	if err := syscall.Setgroups(suppGids); err != nil {
+	if err := syscall.Setgroups(execUser.Sgids); err != nil {
 		return fmt.Errorf("setgroups %s", err)
 	}
 
-	if err := system.Setgid(gid); err != nil {
+	if err := system.Setgid(execUser.Gid); err != nil {
 		return fmt.Errorf("setgid %s", err)
 	}
 
-	if err := system.Setuid(uid); err != nil {
+	if err := system.Setuid(execUser.Uid); err != nil {
 		return fmt.Errorf("setuid %s", err)
 	}
 
 	// if we didn't get HOME already, set it based on the user's HOME
 	if envHome := os.Getenv("HOME"); envHome == "" {
-		if err := os.Setenv("HOME", home); err != nil {
+		if err := os.Setenv("HOME", execUser.Home); err != nil {
 			return fmt.Errorf("set HOME %s", err)
 		}
 	}

+ 3 - 8
vendor/src/github.com/docker/libcontainer/netlink/netlink_linux.go

@@ -1003,28 +1003,23 @@ func AddRoute(destination, source, gateway, device string) error {
 	}
 
 	if source != "" {
-		srcIP, srcNet, err := net.ParseCIDR(source)
+		srcIP := net.ParseIP(source)
 		if err != nil {
-			return fmt.Errorf("source CIDR %s couldn't be parsed", source)
+			return fmt.Errorf("source IP %s couldn't be parsed", source)
 		}
 		srcFamily := getIpFamily(srcIP)
 		if currentFamily != -1 && currentFamily != srcFamily {
 			return fmt.Errorf("source and destination ip were not the same IP family")
 		}
 		currentFamily = srcFamily
-		srcLen, bits := srcNet.Mask.Size()
-		if srcLen == 0 && bits == 0 {
-			return fmt.Errorf("source CIDR %s generated a non-canonical Mask", source)
-		}
 		msg.Family = uint8(srcFamily)
-		msg.Src_len = uint8(srcLen)
 		var srcData []byte
 		if srcFamily == syscall.AF_INET {
 			srcData = srcIP.To4()
 		} else {
 			srcData = srcIP.To16()
 		}
-		rtAttrs = append(rtAttrs, newRtAttr(syscall.RTA_SRC, srcData))
+		rtAttrs = append(rtAttrs, newRtAttr(syscall.RTA_PREFSRC, srcData))
 	}
 
 	if gateway != "" {

+ 28 - 0
vendor/src/github.com/docker/libcontainer/netlink/netlink_linux_test.go

@@ -280,6 +280,34 @@ func TestAddDelNetworkIp(t *testing.T) {
 	}
 }
 
+func TestAddRouteSourceSelection(t *testing.T) {
+	tstIp := "127.1.1.1"
+	tl := testLink{name: "tstEth", linkType: "dummy"}
+
+	addLink(t, tl.name, tl.linkType)
+	defer deleteLink(t, tl.name)
+
+	ip := net.ParseIP(tstIp)
+	mask := net.IPv4Mask(255, 255, 255, 255)
+	ipNet := &net.IPNet{IP: ip, Mask: mask}
+
+	iface, err := net.InterfaceByName(tl.name)
+	if err != nil {
+		t.Fatalf("Lost created link %#v", tl)
+	}
+
+	if err := NetworkLinkAddIp(iface, ip, ipNet); err != nil {
+		t.Fatalf("Could not add IP address %s to interface %#v: %s", ip.String(), iface, err)
+	}
+
+	upLink(t, tl.name)
+	defer downLink(t, tl.name)
+
+	if err := AddRoute("127.0.0.0/8", tstIp, "", tl.name); err != nil {
+		t.Fatalf("Failed to add route with source address")
+	}
+}
+
 func TestCreateVethPair(t *testing.T) {
 	if testing.Short() {
 		return

+ 209 - 0
vendor/src/github.com/docker/libcontainer/sample_configs/route_source_address_selection.json

@@ -0,0 +1,209 @@
+{
+    "capabilities": [
+        "CHOWN",
+        "DAC_OVERRIDE",
+        "FOWNER",
+        "MKNOD",
+        "NET_RAW",
+        "SETGID",
+        "SETUID",
+        "SETFCAP",
+        "SETPCAP",
+        "NET_BIND_SERVICE",
+        "SYS_CHROOT",
+        "KILL"
+    ],
+    "cgroups": {
+        "allowed_devices": [
+            {
+                "cgroup_permissions": "m",
+                "major_number": -1,
+                "minor_number": -1,
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "m",
+                "major_number": -1,
+                "minor_number": -1,
+                "type": 98
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "major_number": 5,
+                "minor_number": 1,
+                "path": "/dev/console",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "major_number": 4,
+                "path": "/dev/tty0",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "major_number": 4,
+                "minor_number": 1,
+                "path": "/dev/tty1",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "major_number": 136,
+                "minor_number": -1,
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "major_number": 5,
+                "minor_number": 2,
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "major_number": 10,
+                "minor_number": 200,
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 3,
+                "path": "/dev/null",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 5,
+                "path": "/dev/zero",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 7,
+                "path": "/dev/full",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 5,
+                "path": "/dev/tty",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 9,
+                "path": "/dev/urandom",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 8,
+                "path": "/dev/random",
+                "type": 99
+            }
+        ],
+        "name": "docker-koye",
+        "parent": "docker"
+    },
+    "restrict_sys": true,
+    "mount_config": {
+        "device_nodes": [
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 3,
+                "path": "/dev/null",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 5,
+                "path": "/dev/zero",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 7,
+                "path": "/dev/full",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 5,
+                "path": "/dev/tty",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 9,
+                "path": "/dev/urandom",
+                "type": 99
+            },
+            {
+                "cgroup_permissions": "rwm",
+                "file_mode": 438,
+                "major_number": 1,
+                "minor_number": 8,
+                "path": "/dev/random",
+                "type": 99
+            }
+        ]
+    },
+    "environment": [
+        "HOME=/",
+        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+        "HOSTNAME=koye",
+        "TERM=xterm"
+    ],
+    "hostname": "koye",
+    "namespaces": {
+        "NEWIPC": true,
+        "NEWNET": true,
+        "NEWNS": true,
+        "NEWPID": true,
+        "NEWUTS": true
+    },
+    "networks": [
+        {
+            "address": "127.0.0.1/0",
+            "gateway": "localhost",
+            "mtu": 1500,
+            "type": "loopback"
+        },
+        {
+            "address": "172.17.0.101/16",
+            "bridge": "docker0",
+            "veth_prefix": "veth",
+            "mtu": 1500,
+            "type": "veth"
+        }
+    ],
+    "routes": [
+        {
+            "destination": "0.0.0.0/0",
+            "source": "172.17.0.101",
+            "gateway": "172.17.42.1",
+            "interface_name": "eth0"
+        }
+    ],
+    "tty": true
+}

+ 108 - 0
vendor/src/github.com/docker/libcontainer/user/lookup.go

@@ -0,0 +1,108 @@
+package user
+
+import (
+	"errors"
+	"fmt"
+	"syscall"
+)
+
+var (
+	// The current operating system does not provide the required data for user lookups.
+	ErrUnsupported = errors.New("user lookup: operating system does not provide passwd-formatted data")
+)
+
+func lookupUser(filter func(u User) bool) (User, error) {
+	// Get operating system-specific passwd reader-closer.
+	passwd, err := GetPasswd()
+	if err != nil {
+		return User{}, err
+	}
+	defer passwd.Close()
+
+	// Get the users.
+	users, err := ParsePasswdFilter(passwd, filter)
+	if err != nil {
+		return User{}, err
+	}
+
+	// No user entries found.
+	if len(users) == 0 {
+		return User{}, fmt.Errorf("no matching entries in passwd file")
+	}
+
+	// Assume the first entry is the "correct" one.
+	return users[0], nil
+}
+
+// CurrentUser looks up the current user by their user id in /etc/passwd. If the
+// user cannot be found (or there is no /etc/passwd file on the filesystem),
+// then CurrentUser returns an error.
+func CurrentUser() (User, error) {
+	return LookupUid(syscall.Getuid())
+}
+
+// LookupUser looks up a user by their username in /etc/passwd. If the user
+// cannot be found (or there is no /etc/passwd file on the filesystem), then
+// LookupUser returns an error.
+func LookupUser(username string) (User, error) {
+	return lookupUser(func(u User) bool {
+		return u.Name == username
+	})
+}
+
+// LookupUid looks up a user by their user id in /etc/passwd. If the user cannot
+// be found (or there is no /etc/passwd file on the filesystem), then LookupId
+// returns an error.
+func LookupUid(uid int) (User, error) {
+	return lookupUser(func(u User) bool {
+		return u.Uid == uid
+	})
+}
+
+func lookupGroup(filter func(g Group) bool) (Group, error) {
+	// Get operating system-specific group reader-closer.
+	group, err := GetGroup()
+	if err != nil {
+		return Group{}, err
+	}
+	defer group.Close()
+
+	// Get the users.
+	groups, err := ParseGroupFilter(group, filter)
+	if err != nil {
+		return Group{}, err
+	}
+
+	// No user entries found.
+	if len(groups) == 0 {
+		return Group{}, fmt.Errorf("no matching entries in group file")
+	}
+
+	// Assume the first entry is the "correct" one.
+	return groups[0], nil
+}
+
+// CurrentGroup looks up the current user's group by their primary group id's
+// entry in /etc/passwd. If the group cannot be found (or there is no
+// /etc/group file on the filesystem), then CurrentGroup returns an error.
+func CurrentGroup() (Group, error) {
+	return LookupGid(syscall.Getgid())
+}
+
+// LookupGroup looks up a group by its name in /etc/group. If the group cannot
+// be found (or there is no /etc/group file on the filesystem), then LookupGroup
+// returns an error.
+func LookupGroup(groupname string) (Group, error) {
+	return lookupGroup(func(g Group) bool {
+		return g.Name == groupname
+	})
+}
+
+// LookupGid looks up a group by its group id in /etc/group. If the group cannot
+// be found (or there is no /etc/group file on the filesystem), then LookupGid
+// returns an error.
+func LookupGid(gid int) (Group, error) {
+	return lookupGroup(func(g Group) bool {
+		return g.Gid == gid
+	})
+}

+ 30 - 0
vendor/src/github.com/docker/libcontainer/user/lookup_unix.go

@@ -0,0 +1,30 @@
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package user
+
+import (
+	"io"
+	"os"
+)
+
+// Unix-specific path to the passwd and group formatted files.
+const (
+	unixPasswdFile = "/etc/passwd"
+	unixGroupFile  = "/etc/group"
+)
+
+func GetPasswdFile() (string, error) {
+	return unixPasswdFile, nil
+}
+
+func GetPasswd() (io.ReadCloser, error) {
+	return os.Open(unixPasswdFile)
+}
+
+func GetGroupFile() (string, error) {
+	return unixGroupFile, nil
+}
+
+func GetGroup() (io.ReadCloser, error) {
+	return os.Open(unixGroupFile)
+}

+ 21 - 0
vendor/src/github.com/docker/libcontainer/user/lookup_unsupported.go

@@ -0,0 +1,21 @@
+// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
+
+package user
+
+import "io"
+
+func GetPasswdFile() (string, error) {
+	return "", ErrUnsupported
+}
+
+func GetPasswd() (io.ReadCloser, error) {
+	return nil, ErrUnsupported
+}
+
+func GetGroupFile() (string, error) {
+	return "", ErrUnsupported
+}
+
+func GetGroup() (io.ReadCloser, error) {
+	return nil, ErrUnsupported
+}

+ 142 - 50
vendor/src/github.com/docker/libcontainer/user/user.go

@@ -69,23 +69,36 @@ func parseLine(line string, v ...interface{}) {
 	}
 }
 
-func ParsePasswd() ([]*User, error) {
-	return ParsePasswdFilter(nil)
+func ParsePasswdFile(path string) ([]User, error) {
+	passwd, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer passwd.Close()
+	return ParsePasswd(passwd)
+}
+
+func ParsePasswd(passwd io.Reader) ([]User, error) {
+	return ParsePasswdFilter(passwd, nil)
 }
 
-func ParsePasswdFilter(filter func(*User) bool) ([]*User, error) {
-	f, err := os.Open("/etc/passwd")
+func ParsePasswdFileFilter(path string, filter func(User) bool) ([]User, error) {
+	passwd, err := os.Open(path)
 	if err != nil {
 		return nil, err
 	}
-	defer f.Close()
-	return parsePasswdFile(f, filter)
+	defer passwd.Close()
+	return ParsePasswdFilter(passwd, filter)
 }
 
-func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
+func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) {
+	if r == nil {
+		return nil, fmt.Errorf("nil source for passwd-formatted data")
+	}
+
 	var (
 		s   = bufio.NewScanner(r)
-		out = []*User{}
+		out = []User{}
 	)
 
 	for s.Scan() {
@@ -103,7 +116,7 @@ func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
 		// Name:Pass:Uid:Gid:Gecos:Home:Shell
 		//  root:x:0:0:root:/root:/bin/bash
 		//  adm:x:3:4:adm:/var/adm:/bin/false
-		p := &User{}
+		p := User{}
 		parseLine(
 			text,
 			&p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell,
@@ -117,23 +130,36 @@ func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
 	return out, nil
 }
 
-func ParseGroup() ([]*Group, error) {
-	return ParseGroupFilter(nil)
+func ParseGroupFile(path string) ([]Group, error) {
+	group, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer group.Close()
+	return ParseGroup(group)
+}
+
+func ParseGroup(group io.Reader) ([]Group, error) {
+	return ParseGroupFilter(group, nil)
 }
 
-func ParseGroupFilter(filter func(*Group) bool) ([]*Group, error) {
-	f, err := os.Open("/etc/group")
+func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) {
+	group, err := os.Open(path)
 	if err != nil {
 		return nil, err
 	}
-	defer f.Close()
-	return parseGroupFile(f, filter)
+	defer group.Close()
+	return ParseGroupFilter(group, filter)
 }
 
-func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
+func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
+	if r == nil {
+		return nil, fmt.Errorf("nil source for group-formatted data")
+	}
+
 	var (
 		s   = bufio.NewScanner(r)
-		out = []*Group{}
+		out = []Group{}
 	)
 
 	for s.Scan() {
@@ -151,7 +177,7 @@ func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
 		// Name:Pass:Gid:List
 		//  root:x:0:root
 		//  adm:x:4:root,adm,daemon
-		p := &Group{}
+		p := Group{}
 		parseLine(
 			text,
 			&p.Name, &p.Pass, &p.Gid, &p.List,
@@ -165,94 +191,160 @@ func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
 	return out, nil
 }
 
-// Given a string like "user", "1000", "user:group", "1000:1000", returns the uid, gid, list of supplementary group IDs, and home directory, if available and/or applicable.
-func GetUserGroupSupplementaryHome(userSpec string, defaultUid, defaultGid int, defaultHome string) (int, int, []int, string, error) {
-	var (
-		uid      = defaultUid
-		gid      = defaultGid
-		suppGids = []int{}
-		home     = defaultHome
+type ExecUser struct {
+	Uid, Gid int
+	Sgids    []int
+	Home     string
+}
 
+// GetExecUserFile is a wrapper for GetExecUser. It reads data from each of the
+// given file paths and uses that data as the arguments to GetExecUser. If the
+// files cannot be opened for any reason, the error is ignored and a nil
+// io.Reader is passed instead.
+func GetExecUserFile(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) {
+	passwd, err := os.Open(passwdPath)
+	if err != nil {
+		passwd = nil
+	} else {
+		defer passwd.Close()
+	}
+
+	group, err := os.Open(groupPath)
+	if err != nil {
+		group = nil
+	} else {
+		defer group.Close()
+	}
+
+	return GetExecUser(userSpec, defaults, passwd, group)
+}
+
+// GetExecUser parses a user specification string (using the passwd and group
+// readers as sources for /etc/passwd and /etc/group data, respectively). In
+// the case of blank fields or missing data from the sources, the values in
+// defaults is used.
+//
+// GetExecUser will return an error if a user or group literal could not be
+// found in any entry in passwd and group respectively.
+//
+// Examples of valid user specifications are:
+//     * ""
+//     * "user"
+//     * "uid"
+//     * "user:group"
+//     * "uid:gid
+//     * "user:gid"
+//     * "uid:group"
+func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) {
+	var (
 		userArg, groupArg string
+		name              string
 	)
 
+	if defaults == nil {
+		defaults = new(ExecUser)
+	}
+
+	// Copy over defaults.
+	user := &ExecUser{
+		Uid:   defaults.Uid,
+		Gid:   defaults.Gid,
+		Sgids: defaults.Sgids,
+		Home:  defaults.Home,
+	}
+
+	// Sgids slice *cannot* be nil.
+	if user.Sgids == nil {
+		user.Sgids = []int{}
+	}
+
 	// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
 	parseLine(userSpec, &userArg, &groupArg)
 
-	users, err := ParsePasswdFilter(func(u *User) bool {
+	users, err := ParsePasswdFilter(passwd, func(u User) bool {
 		if userArg == "" {
-			return u.Uid == uid
+			return u.Uid == user.Uid
 		}
 		return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
 	})
-	if err != nil && !os.IsNotExist(err) {
+	if err != nil && passwd != nil {
 		if userArg == "" {
-			userArg = strconv.Itoa(uid)
+			userArg = strconv.Itoa(user.Uid)
 		}
-		return 0, 0, nil, "", fmt.Errorf("Unable to find user %v: %v", userArg, err)
+		return nil, fmt.Errorf("Unable to find user %v: %v", userArg, err)
 	}
 
 	haveUser := users != nil && len(users) > 0
 	if haveUser {
 		// if we found any user entries that matched our filter, let's take the first one as "correct"
-		uid = users[0].Uid
-		gid = users[0].Gid
-		home = users[0].Home
+		name = users[0].Name
+		user.Uid = users[0].Uid
+		user.Gid = users[0].Gid
+		user.Home = users[0].Home
 	} else if userArg != "" {
 		// we asked for a user but didn't find them...  let's check to see if we wanted a numeric user
-		uid, err = strconv.Atoi(userArg)
+		user.Uid, err = strconv.Atoi(userArg)
 		if err != nil {
 			// not numeric - we have to bail
-			return 0, 0, nil, "", fmt.Errorf("Unable to find user %v", userArg)
+			return nil, fmt.Errorf("Unable to find user %v", userArg)
 		}
-		if uid < minId || uid > maxId {
-			return 0, 0, nil, "", ErrRange
+
+		// Must be inside valid uid range.
+		if user.Uid < minId || user.Uid > maxId {
+			return nil, ErrRange
 		}
 
 		// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
 	}
 
-	if groupArg != "" || (haveUser && users[0].Name != "") {
-		groups, err := ParseGroupFilter(func(g *Group) bool {
+	if groupArg != "" || name != "" {
+		groups, err := ParseGroupFilter(group, func(g Group) bool {
+			// Explicit group format takes precedence.
 			if groupArg != "" {
 				return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
 			}
+
+			// Check if user is a member.
 			for _, u := range g.List {
-				if u == users[0].Name {
+				if u == name {
 					return true
 				}
 			}
+
 			return false
 		})
-		if err != nil && !os.IsNotExist(err) {
-			return 0, 0, nil, "", fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
+		if err != nil && group != nil {
+			return nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
 		}
 
 		haveGroup := groups != nil && len(groups) > 0
 		if groupArg != "" {
 			if haveGroup {
 				// if we found any group entries that matched our filter, let's take the first one as "correct"
-				gid = groups[0].Gid
+				user.Gid = groups[0].Gid
 			} else {
 				// we asked for a group but didn't find id...  let's check to see if we wanted a numeric group
-				gid, err = strconv.Atoi(groupArg)
+				user.Gid, err = strconv.Atoi(groupArg)
 				if err != nil {
 					// not numeric - we have to bail
-					return 0, 0, nil, "", fmt.Errorf("Unable to find group %v", groupArg)
+					return nil, fmt.Errorf("Unable to find group %v", groupArg)
 				}
-				if gid < minId || gid > maxId {
-					return 0, 0, nil, "", ErrRange
+
+				// Ensure gid is inside gid range.
+				if user.Gid < minId || user.Gid > maxId {
+					return nil, ErrRange
 				}
 
 				// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
 			}
 		} else if haveGroup {
-			suppGids = make([]int, len(groups))
+			// If implicit group format, fill supplementary gids.
+			user.Sgids = make([]int, len(groups))
 			for i, group := range groups {
-				suppGids[i] = group.Gid
+				user.Sgids[i] = group.Gid
 			}
 		}
 	}
 
-	return uid, gid, suppGids, home, nil
+	return user, nil
 }

+ 260 - 2
vendor/src/github.com/docker/libcontainer/user/user_test.go

@@ -1,6 +1,8 @@
 package user
 
 import (
+	"io"
+	"reflect"
 	"strings"
 	"testing"
 )
@@ -54,7 +56,7 @@ func TestUserParseLine(t *testing.T) {
 }
 
 func TestUserParsePasswd(t *testing.T) {
-	users, err := parsePasswdFile(strings.NewReader(`
+	users, err := ParsePasswdFilter(strings.NewReader(`
 root:x:0:0:root:/root:/bin/bash
 adm:x:3:4:adm:/var/adm:/bin/false
 this is just some garbage data
@@ -74,7 +76,7 @@ this is just some garbage data
 }
 
 func TestUserParseGroup(t *testing.T) {
-	groups, err := parseGroupFile(strings.NewReader(`
+	groups, err := ParseGroupFilter(strings.NewReader(`
 root:x:0:root
 adm:x:4:root,adm,daemon
 this is just some garbage data
@@ -92,3 +94,259 @@ this is just some garbage data
 		t.Fatalf("Expected groups[1] to be 4 - adm - 3 members, got %v - %v - %v", groups[1].Gid, groups[1].Name, len(groups[1].List))
 	}
 }
+
+func TestValidGetExecUser(t *testing.T) {
+	const passwdContent = `
+root:x:0:0:root user:/root:/bin/bash
+adm:x:42:43:adm:/var/adm:/bin/false
+this is just some garbage data
+`
+	const groupContent = `
+root:x:0:root
+adm:x:43:
+grp:x:1234:root,adm
+this is just some garbage data
+`
+	defaultExecUser := ExecUser{
+		Uid:   8888,
+		Gid:   8888,
+		Sgids: []int{8888},
+		Home:  "/8888",
+	}
+
+	tests := []struct {
+		ref      string
+		expected ExecUser
+	}{
+		{
+			ref: "root",
+			expected: ExecUser{
+				Uid:   0,
+				Gid:   0,
+				Sgids: []int{0, 1234},
+				Home:  "/root",
+			},
+		},
+		{
+			ref: "adm",
+			expected: ExecUser{
+				Uid:   42,
+				Gid:   43,
+				Sgids: []int{1234},
+				Home:  "/var/adm",
+			},
+		},
+		{
+			ref: "root:adm",
+			expected: ExecUser{
+				Uid:   0,
+				Gid:   43,
+				Sgids: defaultExecUser.Sgids,
+				Home:  "/root",
+			},
+		},
+		{
+			ref: "adm:1234",
+			expected: ExecUser{
+				Uid:   42,
+				Gid:   1234,
+				Sgids: defaultExecUser.Sgids,
+				Home:  "/var/adm",
+			},
+		},
+		{
+			ref: "42:1234",
+			expected: ExecUser{
+				Uid:   42,
+				Gid:   1234,
+				Sgids: defaultExecUser.Sgids,
+				Home:  "/var/adm",
+			},
+		},
+		{
+			ref: "1337:1234",
+			expected: ExecUser{
+				Uid:   1337,
+				Gid:   1234,
+				Sgids: defaultExecUser.Sgids,
+				Home:  defaultExecUser.Home,
+			},
+		},
+		{
+			ref: "1337",
+			expected: ExecUser{
+				Uid:   1337,
+				Gid:   defaultExecUser.Gid,
+				Sgids: defaultExecUser.Sgids,
+				Home:  defaultExecUser.Home,
+			},
+		},
+		{
+			ref: "",
+			expected: ExecUser{
+				Uid:   defaultExecUser.Uid,
+				Gid:   defaultExecUser.Gid,
+				Sgids: defaultExecUser.Sgids,
+				Home:  defaultExecUser.Home,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		passwd := strings.NewReader(passwdContent)
+		group := strings.NewReader(groupContent)
+
+		execUser, err := GetExecUser(test.ref, &defaultExecUser, passwd, group)
+		if err != nil {
+			t.Logf("got unexpected error when parsing '%s': %s", test.ref, err.Error())
+			t.Fail()
+			continue
+		}
+
+		if !reflect.DeepEqual(test.expected, *execUser) {
+			t.Logf("got:      %#v", execUser)
+			t.Logf("expected: %#v", test.expected)
+			t.Fail()
+			continue
+		}
+	}
+}
+
+func TestInvalidGetExecUser(t *testing.T) {
+	const passwdContent = `
+root:x:0:0:root user:/root:/bin/bash
+adm:x:42:43:adm:/var/adm:/bin/false
+this is just some garbage data
+`
+	const groupContent = `
+root:x:0:root
+adm:x:43:
+grp:x:1234:root,adm
+this is just some garbage data
+`
+
+	tests := []string{
+		// No such user/group.
+		"notuser",
+		"notuser:notgroup",
+		"root:notgroup",
+		"notuser:adm",
+		"8888:notgroup",
+		"notuser:8888",
+
+		// Invalid user/group values.
+		"-1:0",
+		"0:-3",
+		"-5:-2",
+	}
+
+	for _, test := range tests {
+		passwd := strings.NewReader(passwdContent)
+		group := strings.NewReader(groupContent)
+
+		execUser, err := GetExecUser(test, nil, passwd, group)
+		if err == nil {
+			t.Logf("got unexpected success when parsing '%s': %#v", test, execUser)
+			t.Fail()
+			continue
+		}
+	}
+}
+
+func TestGetExecUserNilSources(t *testing.T) {
+	const passwdContent = `
+root:x:0:0:root user:/root:/bin/bash
+adm:x:42:43:adm:/var/adm:/bin/false
+this is just some garbage data
+`
+	const groupContent = `
+root:x:0:root
+adm:x:43:
+grp:x:1234:root,adm
+this is just some garbage data
+`
+
+	defaultExecUser := ExecUser{
+		Uid:   8888,
+		Gid:   8888,
+		Sgids: []int{8888},
+		Home:  "/8888",
+	}
+
+	tests := []struct {
+		ref           string
+		passwd, group bool
+		expected      ExecUser
+	}{
+		{
+			ref:    "",
+			passwd: false,
+			group:  false,
+			expected: ExecUser{
+				Uid:   8888,
+				Gid:   8888,
+				Sgids: []int{8888},
+				Home:  "/8888",
+			},
+		},
+		{
+			ref:    "root",
+			passwd: true,
+			group:  false,
+			expected: ExecUser{
+				Uid:   0,
+				Gid:   0,
+				Sgids: []int{8888},
+				Home:  "/root",
+			},
+		},
+		{
+			ref:    "0",
+			passwd: false,
+			group:  false,
+			expected: ExecUser{
+				Uid:   0,
+				Gid:   8888,
+				Sgids: []int{8888},
+				Home:  "/8888",
+			},
+		},
+		{
+			ref:    "0:0",
+			passwd: false,
+			group:  false,
+			expected: ExecUser{
+				Uid:   0,
+				Gid:   0,
+				Sgids: []int{8888},
+				Home:  "/8888",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		var passwd, group io.Reader
+
+		if test.passwd {
+			passwd = strings.NewReader(passwdContent)
+		}
+
+		if test.group {
+			group = strings.NewReader(groupContent)
+		}
+
+		execUser, err := GetExecUser(test.ref, &defaultExecUser, passwd, group)
+		if err != nil {
+			t.Logf("got unexpected error when parsing '%s': %s", test.ref, err.Error())
+			t.Fail()
+			continue
+		}
+
+		if !reflect.DeepEqual(test.expected, *execUser) {
+			t.Logf("got:      %#v", execUser)
+			t.Logf("expected: %#v", test.expected)
+			t.Fail()
+			continue
+		}
+	}
+}