diff options
-rw-r--r-- | cmd/podman/containers/runlabel.go | 5 | ||||
-rw-r--r-- | docs/source/markdown/podman-login.1.md | 12 | ||||
-rw-r--r-- | docs/source/markdown/podman-logout.1.md | 4 | ||||
-rw-r--r-- | libpod/container_internal.go | 2 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 417 | ||||
-rw-r--r-- | libpod/container_internal_linux_test.go | 34 | ||||
-rw-r--r-- | libpod/options.go | 12 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers_runlabel.go | 32 | ||||
-rw-r--r-- | pkg/domain/infra/abi/images.go | 10 | ||||
-rw-r--r-- | pkg/domain/infra/abi/play.go | 6 | ||||
-rw-r--r-- | test/e2e/play_kube_test.go | 44 | ||||
-rw-r--r-- | test/e2e/run_passwd_test.go | 54 | ||||
-rw-r--r-- | test/e2e/run_userns_test.go | 7 | ||||
-rw-r--r-- | test/e2e/runlabel_test.go | 7 | ||||
-rw-r--r-- | test/system/030-run.bats | 18 | ||||
-rw-r--r-- | test/system/120-load.bats | 34 |
16 files changed, 566 insertions, 132 deletions
diff --git a/cmd/podman/containers/runlabel.go b/cmd/podman/containers/runlabel.go index 81eec2a42..5ee8c9d6c 100644 --- a/cmd/podman/containers/runlabel.go +++ b/cmd/podman/containers/runlabel.go @@ -30,7 +30,7 @@ var ( RunE: runlabel, Args: cobra.MinimumNArgs(2), Example: `podman container runlabel run imageID - podman container runlabel --pull install imageID arg1 arg2 + podman container runlabel install imageID arg1 arg2 podman container runlabel --display run myImage`, } ) @@ -51,7 +51,7 @@ func init() { flags.StringVar(&runlabelOptions.Optional1, "opt1", "", "Optional parameter to pass for install") flags.StringVar(&runlabelOptions.Optional2, "opt2", "", "Optional parameter to pass for install") flags.StringVar(&runlabelOptions.Optional3, "opt3", "", "Optional parameter to pass for install") - flags.BoolP("pull", "p", false, "Pull the image if it does not exist locally prior to executing the label contents") + flags.BoolVarP(&runlabelOptions.Pull, "pull", "p", true, "Pull the image if it does not exist locally prior to executing the label contents") flags.BoolVarP(&runlabelOptions.Quiet, "quiet", "q", false, "Suppress output information when installing images") flags.BoolVar(&runlabelOptions.Replace, "replace", false, "Replace existing container with a new one from the image") flags.StringVar(&runlabelOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)") @@ -61,6 +61,7 @@ func init() { _ = flags.MarkHidden("opt1") _ = flags.MarkHidden("opt2") _ = flags.MarkHidden("opt3") + _ = flags.MarkHidden("pull") _ = flags.MarkHidden("signature-policy") if err := flags.MarkDeprecated("pull", "podman will pull if not found in local storage"); err != nil { diff --git a/docs/source/markdown/podman-login.1.md b/docs/source/markdown/podman-login.1.md index 79c7ff640..efc7f05e2 100644 --- a/docs/source/markdown/podman-login.1.md +++ b/docs/source/markdown/podman-login.1.md @@ -12,9 +12,13 @@ and password. If the registry is not specified, the first registry under [regist from registries.conf will be used. **podman login** reads in the username and password from STDIN. The username and password can also be set using the **username** and **password** flags. The path of the authentication file can be specified by the user by setting the **authfile** -flag. The default path used is **${XDG\_RUNTIME\_DIR}/containers/auth.json**. If there is a valid -username and password in the **authfile** , Podman will use those existing credentials if the user does not pass in a username. -If those credentials are not present, Podman will then use any existing credentials found in **$HOME/.docker/config.json**. +flag. The default path for reading and writing credentials is **${XDG\_RUNTIME\_DIR}/containers/auth.json**. +Podman will use existing credentials if the user does not pass in a username. +Podman will first search for the username and password in the **${XDG\_RUNTIME\_DIR}/containers/auth.json**, if they are not valid, +Podman will then use any existing credentials found in **$HOME/.docker/config.json**. +If those credentials are not present, Podman will create **${XDG\_RUNTIME\_DIR}/containers/auth.json** (if the file does not exist) and +will then store the username and password from STDIN as a base64 encoded string in it. +For more details about format and configurations of the auth,json file, please refer to containers-auth.json(5) **podman [GLOBAL OPTIONS]** @@ -104,7 +108,7 @@ Login Succeeded! ``` ## SEE ALSO -podman(1), podman-logout(1) +podman(1), podman-logout(1), containers-auth.json(5) ## HISTORY August 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-logout.1.md b/docs/source/markdown/podman-logout.1.md index 8b9f75760..0ff954d43 100644 --- a/docs/source/markdown/podman-logout.1.md +++ b/docs/source/markdown/podman-logout.1.md @@ -10,7 +10,7 @@ podman\-logout - Logout of a container registry **podman logout** logs out of a specified registry server by deleting the cached credentials stored in the **auth.json** file. If the registry is not specified, the first registry under [registries.search] from registries.conf will be used. The path of the authentication file can be overridden by the user by setting the **authfile** flag. -The default path used is **${XDG\_RUNTIME\_DIR}/containers/auth.json**. +The default path used is **${XDG\_RUNTIME\_DIR}/containers/auth.json**. For more details about format and configurations of the auth,json file, please refer to containers-auth.json(5) All the cached credentials can be removed by setting the **all** flag. **podman [GLOBAL OPTIONS]** @@ -54,7 +54,7 @@ Remove login credentials for all registries ``` ## SEE ALSO -podman(1), podman-login(1) +podman(1), podman-login(1), containers-auth.json(5) ## HISTORY August 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/libpod/container_internal.go b/libpod/container_internal.go index c3f07a48b..5a0a0edfa 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -380,6 +380,8 @@ func (c *Container) setupStorageMapping(dest, from *storage.IDMappingOptions) { } dest.GIDMap = append(dest.GIDMap, g) } + dest.HostUIDMapping = false + dest.HostGIDMapping = false } } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 605b526a4..86a28c176 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -36,7 +36,7 @@ import ( "github.com/containers/podman/v2/utils" "github.com/containers/storage/pkg/archive" securejoin "github.com/cyphar/filepath-securejoin" - User "github.com/opencontainers/runc/libcontainer/user" + runcuser "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" @@ -1276,7 +1276,7 @@ func (c *Container) makeBindMounts() error { // SHM is always added when we mount the container c.state.BindMounts["/dev/shm"] = c.config.ShmDir - newPasswd, err := c.generatePasswd() + newPasswd, newGroup, err := c.generatePasswdAndGroup() if err != nil { return errors.Wrapf(err, "error creating temporary passwd file for container %s", c.ID()) } @@ -1286,9 +1286,16 @@ func (c *Container) makeBindMounts() error { // If it already exists, delete so we can recreate delete(c.state.BindMounts, "/etc/passwd") } - logrus.Debugf("adding entry to /etc/passwd for non existent default user") c.state.BindMounts["/etc/passwd"] = newPasswd } + if newGroup != "" { + // Make /etc/group + if _, ok := c.state.BindMounts["/etc/group"]; ok { + // If it already exists, delete so we can recreate + delete(c.state.BindMounts, "/etc/group") + } + c.state.BindMounts["/etc/group"] = newGroup + } // Make /etc/hostname // This should never change, so no need to recreate if it exists @@ -1507,23 +1514,171 @@ func (c *Container) getHosts() string { return hosts } +// generateGroupEntry generates an entry or entries into /etc/group as +// required by container configuration. +// Generatlly speaking, we will make an entry under two circumstances: +// 1. The container is started as a specific user:group, and that group is both +// numeric, and does not already exist in /etc/group. +// 2. It is requested that Libpod add the group that launched Podman to +// /etc/group via AddCurrentUserPasswdEntry (though this does not trigger if +// the group in question already exists in /etc/passwd). +// Returns group entry (as a string that can be appended to /etc/group) and any +// error that occurred. +func (c *Container) generateGroupEntry() (string, error) { + groupString := "" + + // Things we *can't* handle: adding the user we added in + // generatePasswdEntry to any *existing* groups. + addedGID := 0 + if c.config.AddCurrentUserPasswdEntry { + entry, gid, err := c.generateCurrentUserGroupEntry() + if err != nil { + return "", err + } + groupString += entry + addedGID = gid + } + if c.config.User != "" { + entry, _, err := c.generateUserGroupEntry(addedGID) + if err != nil { + return "", err + } + groupString += entry + } + + return groupString, nil +} + +// Make an entry in /etc/group for the group of the user running podman iff we +// are rootless. +func (c *Container) generateCurrentUserGroupEntry() (string, int, error) { + gid := rootless.GetRootlessGID() + if gid == 0 { + return "", 0, nil + } + + g, err := user.LookupGroupId(strconv.Itoa(gid)) + if err != nil { + return "", 0, errors.Wrapf(err, "failed to get current group") + } + + // Lookup group name to see if it exists in the image. + _, err = lookup.GetGroup(c.state.Mountpoint, g.Name) + if err != runcuser.ErrNoGroupEntries { + return "", 0, err + } + + // Lookup GID to see if it exists in the image. + _, err = lookup.GetGroup(c.state.Mountpoint, g.Gid) + if err != runcuser.ErrNoGroupEntries { + return "", 0, err + } + + // We need to get the username of the rootless user so we can add it to + // the group. + username := "" + uid := rootless.GetRootlessUID() + if uid != 0 { + u, err := user.LookupId(strconv.Itoa(uid)) + if err != nil { + return "", 0, errors.Wrapf(err, "failed to get current user to make group entry") + } + username = u.Username + } + + // Make the entry. + return fmt.Sprintf("%s:x:%s:%s\n", g.Name, g.Gid, username), gid, nil +} + +// Make an entry in /etc/group for the group the container was specified to run +// as. +func (c *Container) generateUserGroupEntry(addedGID int) (string, int, error) { + if c.config.User == "" { + return "", 0, nil + } + + splitUser := strings.SplitN(c.config.User, ":", 2) + group := splitUser[0] + if len(splitUser) > 1 { + group = splitUser[1] + } + + gid, err := strconv.ParseUint(group, 10, 32) + if err != nil { + return "", 0, nil + } + + if addedGID != 0 && addedGID == int(gid) { + return "", 0, nil + } + + // Check if the group already exists + _, err = lookup.GetGroup(c.state.Mountpoint, group) + if err != runcuser.ErrNoGroupEntries { + return "", 0, err + } + + return fmt.Sprintf("%d:x:%d:%s\n", gid, gid, splitUser[0]), int(gid), nil +} + +// generatePasswdEntry generates an entry or entries into /etc/passwd as +// required by container configuration. +// Generally speaking, we will make an entry under two circumstances: +// 1. The container is started as a specific user who is not in /etc/passwd. +// This only triggers if the user is given as a *numeric* ID. +// 2. It is requested that Libpod add the user that launched Podman to +// /etc/passwd via AddCurrentUserPasswdEntry (though this does not trigger if +// the user in question already exists in /etc/passwd) or the UID to be added +// is 0). +// Returns password entry (as a string that can be appended to /etc/passwd) and +// any error that occurred. +func (c *Container) generatePasswdEntry() (string, error) { + passwdString := "" + + addedUID := 0 + if c.config.AddCurrentUserPasswdEntry { + entry, uid, _, err := c.generateCurrentUserPasswdEntry() + if err != nil { + return "", err + } + passwdString += entry + addedUID = uid + } + if c.config.User != "" { + entry, _, _, err := c.generateUserPasswdEntry(addedUID) + if err != nil { + return "", err + } + passwdString += entry + } + + return passwdString, nil +} + // generateCurrentUserPasswdEntry generates an /etc/passwd entry for the user -// running the container engine -func (c *Container) generateCurrentUserPasswdEntry() (string, error) { +// running the container engine. +// Returns a passwd entry for the user, and the UID and GID of the added entry. +func (c *Container) generateCurrentUserPasswdEntry() (string, int, int, error) { uid := rootless.GetRootlessUID() if uid == 0 { - return "", nil + return "", 0, 0, nil } - u, err := user.LookupId(strconv.Itoa(rootless.GetRootlessUID())) + u, err := user.LookupId(strconv.Itoa(uid)) if err != nil { - return "", errors.Wrapf(err, "failed to get current user") + return "", 0, 0, errors.Wrapf(err, "failed to get current user") } // Lookup the user to see if it exists in the container image. _, err = lookup.GetUser(c.state.Mountpoint, u.Username) - if err != User.ErrNoPasswdEntries { - return "", err + if err != runcuser.ErrNoPasswdEntries { + return "", 0, 0, err + } + + // Lookup the UID to see if it exists in the container image. + _, err = lookup.GetUser(c.state.Mountpoint, u.Uid) + if err != runcuser.ErrNoPasswdEntries { + return "", 0, 0, err } // If the user's actual home directory exists, or was mounted in - use @@ -1533,18 +1688,22 @@ func (c *Container) generateCurrentUserPasswdEntry() (string, error) { homeDir = u.HomeDir } - return fmt.Sprintf("%s:x:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Username, homeDir), nil + return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Username, homeDir), uid, rootless.GetRootlessGID(), nil } // generateUserPasswdEntry generates an /etc/passwd entry for the container user // to run in the container. -func (c *Container) generateUserPasswdEntry() (string, error) { +// The UID and GID of the added entry will also be returned. +// Accepts one argument, that being any UID that has already been added to the +// passwd file by other functions; if it matches the UID we were given, we don't +// need to do anything. +func (c *Container) generateUserPasswdEntry(addedUID int) (string, int, int, error) { var ( groupspec string gid int ) if c.config.User == "" { - return "", nil + return "", 0, 0, nil } splitSpec := strings.SplitN(c.config.User, ":", 2) userspec := splitSpec[0] @@ -1554,13 +1713,17 @@ func (c *Container) generateUserPasswdEntry() (string, error) { // If a non numeric User, then don't generate passwd uid, err := strconv.ParseUint(userspec, 10, 32) if err != nil { - return "", nil + return "", 0, 0, nil + } + + if addedUID != 0 && int(uid) == addedUID { + return "", 0, 0, nil } // Lookup the user to see if it exists in the container image _, err = lookup.GetUser(c.state.Mountpoint, userspec) - if err != User.ErrNoPasswdEntries { - return "", err + if err != runcuser.ErrNoPasswdEntries { + return "", 0, 0, err } if groupspec != "" { @@ -1570,96 +1733,180 @@ func (c *Container) generateUserPasswdEntry() (string, error) { } else { group, err := lookup.GetGroup(c.state.Mountpoint, groupspec) if err != nil { - return "", errors.Wrapf(err, "unable to get gid %s from group file", groupspec) + return "", 0, 0, errors.Wrapf(err, "unable to get gid %s from group file", groupspec) } gid = group.Gid } } - return fmt.Sprintf("%d:x:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), nil + return fmt.Sprintf("%d:*:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), int(uid), gid, nil } -// generatePasswd generates a container specific passwd file, -// iff g.config.User is a number -func (c *Container) generatePasswd() (string, error) { +// generatePasswdAndGroup generates container-specific passwd and group files +// iff g.config.User is a number or we are configured to make a passwd entry for +// the current user. +// Returns path to file to mount at /etc/passwd, path to file to mount at +// /etc/group, and any error that occurred. If no passwd/group file were +// required, the empty string will be returned for those path (this may occur +// even if no error happened). +// This may modify the mounted container's /etc/passwd and /etc/group instead of +// making copies to bind-mount in, so we don't break useradd (it wants to make a +// copy of /etc/passwd and rename the copy to /etc/passwd, which is impossible +// with a bind mount). This is done in cases where the container is *not* +// read-only. In this case, the function will return nothing ("", "", nil). +func (c *Container) generatePasswdAndGroup() (string, string, error) { if !c.config.AddCurrentUserPasswdEntry && c.config.User == "" { - return "", nil + return "", "", nil } + + needPasswd := true + needGroup := true + + // First, check if there's a mount at /etc/passwd or group, we don't + // want to interfere with user mounts. if MountExists(c.config.Spec.Mounts, "/etc/passwd") { - return "", nil + needPasswd = false } - // Re-use passwd if possible - passwdPath := filepath.Join(c.config.StaticDir, "passwd") - if _, err := os.Stat(passwdPath); err == nil { - return passwdPath, nil + if MountExists(c.config.Spec.Mounts, "/etc/group") { + needGroup = false } - // Check if container has a /etc/passwd - if it doesn't do nothing. - passwdPath, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") - if err != nil { - return "", errors.Wrapf(err, "error creating path to container %s /etc/passwd", c.ID()) + + // Next, check if we already made the files. If we didn, don't need to + // do anything more. + if needPasswd { + passwdPath := filepath.Join(c.config.StaticDir, "passwd") + if _, err := os.Stat(passwdPath); err == nil { + needPasswd = false + } } - if _, err := os.Stat(passwdPath); err != nil { - if os.IsNotExist(err) { - return "", nil + if needGroup { + groupPath := filepath.Join(c.config.StaticDir, "group") + if _, err := os.Stat(groupPath); err == nil { + needGroup = false } - return "", errors.Wrapf(err, "unable to access container %s /etc/passwd", c.ID()) } - pwd := "" - if c.config.User != "" { - entry, err := c.generateUserPasswdEntry() + + // Next, check if the container even has a /etc/passwd or /etc/group. + // If it doesn't we don't want to create them ourselves. + if needPasswd { + exists, err := c.checkFileExistsInRootfs("/etc/passwd") if err != nil { - return "", err + return "", "", err } - pwd += entry + needPasswd = exists } - if c.config.AddCurrentUserPasswdEntry { - entry, err := c.generateCurrentUserPasswdEntry() + if needGroup { + exists, err := c.checkFileExistsInRootfs("/etc/group") if err != nil { - return "", err + return "", "", err } - pwd += entry + needGroup = exists } - if pwd == "" { - return "", nil + + // If we don't need a /etc/passwd or /etc/group at this point we can + // just return. + if !needPasswd && !needGroup { + return "", "", nil } - // If we are *not* read-only - edit /etc/passwd in the container. - // This is *gross* (shows up in changes to the container, will be - // committed to images based on the container) but it actually allows us - // to add users to the container (a bind mount breaks useradd). - // We should never get here twice, because generateUserPasswdEntry will - // not return anything if the user already exists in /etc/passwd. - if !c.IsReadOnly() { - containerPasswd, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") + passwdPath := "" + groupPath := "" + + ro := c.IsReadOnly() + + if needPasswd { + passwdEntry, err := c.generatePasswdEntry() if err != nil { - return "", errors.Wrapf(err, "error looking up location of container %s /etc/passwd", c.ID()) + return "", "", err } - f, err := os.OpenFile(containerPasswd, os.O_APPEND|os.O_WRONLY, 0600) + needsWrite := passwdEntry != "" + switch { + case ro && needsWrite: + logrus.Debugf("Making /etc/passwd for container %s", c.ID()) + originPasswdFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") + if err != nil { + return "", "", errors.Wrapf(err, "error creating path to container %s /etc/passwd", c.ID()) + } + orig, err := ioutil.ReadFile(originPasswdFile) + if err != nil && !os.IsNotExist(err) { + return "", "", errors.Wrapf(err, "unable to read passwd file %s", originPasswdFile) + } + passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+passwdEntry) + if err != nil { + return "", "", errors.Wrapf(err, "failed to create temporary passwd file") + } + if err := os.Chmod(passwdFile, 0644); err != nil { + return "", "", err + } + passwdPath = passwdFile + case !ro && needsWrite: + logrus.Debugf("Modifying container %s /etc/passwd", c.ID()) + containerPasswd, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") + if err != nil { + return "", "", errors.Wrapf(err, "error looking up location of container %s /etc/passwd", c.ID()) + } + + f, err := os.OpenFile(containerPasswd, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return "", "", errors.Wrapf(err, "error opening container %s /etc/passwd", c.ID()) + } + defer f.Close() + + if _, err := f.WriteString(passwdEntry); err != nil { + return "", "", errors.Wrapf(err, "unable to append to container %s /etc/passwd", c.ID()) + } + default: + logrus.Debugf("Not modifying container %s /etc/passwd", c.ID()) + } + } + if needGroup { + groupEntry, err := c.generateGroupEntry() if err != nil { - return "", errors.Wrapf(err, "error opening container %s /etc/passwd", c.ID()) + return "", "", err } - defer f.Close() - if _, err := f.WriteString(pwd); err != nil { - return "", errors.Wrapf(err, "unable to append to container %s /etc/passwd", c.ID()) - } + needsWrite := groupEntry != "" + switch { + case ro && needsWrite: + logrus.Debugf("Making /etc/group for container %s", c.ID()) + originGroupFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group") + if err != nil { + return "", "", errors.Wrapf(err, "error creating path to container %s /etc/group", c.ID()) + } + orig, err := ioutil.ReadFile(originGroupFile) + if err != nil && !os.IsNotExist(err) { + return "", "", errors.Wrapf(err, "unable to read group file %s", originGroupFile) + } + groupFile, err := c.writeStringToStaticDir("group", string(orig)+groupEntry) + if err != nil { + return "", "", errors.Wrapf(err, "failed to create temporary group file") + } + if err := os.Chmod(groupFile, 0644); err != nil { + return "", "", err + } + groupPath = groupFile + case !ro && needsWrite: + logrus.Debugf("Modifying container %s /etc/group", c.ID()) + containerGroup, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group") + if err != nil { + return "", "", errors.Wrapf(err, "error looking up location of container %s /etc/group", c.ID()) + } - return "", nil - } + f, err := os.OpenFile(containerGroup, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return "", "", errors.Wrapf(err, "error opening container %s /etc/group", c.ID()) + } + defer f.Close() - originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd") - orig, err := ioutil.ReadFile(originPasswdFile) - if err != nil && !os.IsNotExist(err) { - return "", errors.Wrapf(err, "unable to read passwd file %s", originPasswdFile) - } - passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+pwd) - if err != nil { - return "", errors.Wrapf(err, "failed to create temporary passwd file") - } - if err := os.Chmod(passwdFile, 0644); err != nil { - return "", err + if _, err := f.WriteString(groupEntry); err != nil { + return "", "", errors.Wrapf(err, "unable to append to container %s /etc/group", c.ID()) + } + default: + logrus.Debugf("Not modifying container %s /etc/group", c.ID()) + } } - return passwdFile, nil + + return passwdPath, groupPath, nil } func (c *Container) copyOwnerAndPerms(source, dest string) error { @@ -1751,3 +1998,23 @@ func (c *Container) copyTimezoneFile(zonePath string) (string, error) { func (c *Container) cleanupOverlayMounts() error { return overlay.CleanupContent(c.config.StaticDir) } + +// Check if a file exists at the given path in the container's root filesystem. +// Container must already be mounted for this to be used. +func (c *Container) checkFileExistsInRootfs(file string) (bool, error) { + checkPath, err := securejoin.SecureJoin(c.state.Mountpoint, file) + if err != nil { + return false, errors.Wrapf(err, "cannot create path to container %s file %q", c.ID(), file) + } + stat, err := os.Stat(checkPath) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, errors.Wrapf(err, "error accessing container %s file %q", c.ID(), file) + } + if stat.IsDir() { + return false, nil + } + return true, nil +} diff --git a/libpod/container_internal_linux_test.go b/libpod/container_internal_linux_test.go index 41c22fb45..1465ffbea 100644 --- a/libpod/container_internal_linux_test.go +++ b/libpod/container_internal_linux_test.go @@ -29,16 +29,42 @@ func TestGenerateUserPasswdEntry(t *testing.T) { Mountpoint: "/does/not/exist/tmp/", }, } - user, err := c.generateUserPasswdEntry() + user, _, _, err := c.generateUserPasswdEntry(0) if err != nil { t.Fatal(err) } - assert.Equal(t, user, "123:x:123:456:container user:/:/bin/sh\n") + assert.Equal(t, user, "123:*:123:456:container user:/:/bin/sh\n") c.config.User = "567" - user, err = c.generateUserPasswdEntry() + user, _, _, err = c.generateUserPasswdEntry(0) if err != nil { t.Fatal(err) } - assert.Equal(t, user, "567:x:567:0:container user:/:/bin/sh\n") + assert.Equal(t, user, "567:*:567:0:container user:/:/bin/sh\n") +} + +func TestGenerateUserGroupEntry(t *testing.T) { + c := Container{ + config: &ContainerConfig{ + Spec: &spec.Spec{}, + ContainerSecurityConfig: ContainerSecurityConfig{ + User: "123:456", + }, + }, + state: &ContainerState{ + Mountpoint: "/does/not/exist/tmp/", + }, + } + group, _, err := c.generateUserGroupEntry(0) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, group, "456:x:456:123\n") + + c.config.User = "567" + group, _, err = c.generateUserGroupEntry(0) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, group, "567:x:567:567\n") } diff --git a/libpod/options.go b/libpod/options.go index dccbb8741..7eec530ea 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -18,6 +18,7 @@ import ( "github.com/containers/storage" "github.com/containers/storage/pkg/idtools" "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -897,6 +898,17 @@ func WithUserNSFrom(nsCtr *Container) CtrCreateOption { ctr.config.UserNsCtr = nsCtr.ID() ctr.config.IDMappings = nsCtr.config.IDMappings + g := generate.NewFromSpec(ctr.config.Spec) + + g.ClearLinuxUIDMappings() + for _, uidmap := range nsCtr.config.IDMappings.UIDMap { + g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) + } + g.ClearLinuxGIDMappings() + for _, gidmap := range nsCtr.config.IDMappings.GIDMap { + g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) + } + ctr.config.IDMappings = nsCtr.config.IDMappings return nil } } diff --git a/pkg/domain/infra/abi/containers_runlabel.go b/pkg/domain/infra/abi/containers_runlabel.go index 3983ba3a8..30a5a55b8 100644 --- a/pkg/domain/infra/abi/containers_runlabel.go +++ b/pkg/domain/infra/abi/containers_runlabel.go @@ -7,12 +7,10 @@ import ( "path/filepath" "strings" - "github.com/containers/image/v5/types" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/libpod/image" "github.com/containers/podman/v2/pkg/domain/entities" envLib "github.com/containers/podman/v2/pkg/env" - "github.com/containers/podman/v2/pkg/util" "github.com/containers/podman/v2/utils" "github.com/google/shlex" "github.com/pkg/errors" @@ -89,29 +87,17 @@ func (ic *ContainerEngine) runlabelImage(ctx context.Context, label string, imag // Fallthrough and pull! } - // Parse credentials if specified. - var credentials *types.DockerAuthConfig - if options.Credentials != "" { - credentials, err = util.ParseRegistryCreds(options.Credentials) - if err != nil { - return nil, err - } - } - - // Suppress pull progress bars if requested. - pullOutput := os.Stdout - if options.Quiet { - pullOutput = nil // c/image/copy takes care of the rest + pullOptions := entities.ImagePullOptions{ + Quiet: options.Quiet, + CertDir: options.CertDir, + SkipTLSVerify: options.SkipTLSVerify, + SignaturePolicy: options.SignaturePolicy, + Authfile: options.Authfile, } - - // Pull the image. - dockerRegistryOptions := image.DockerRegistryOptions{ - DockerCertPath: options.CertDir, - DockerInsecureSkipTLSVerify: options.SkipTLSVerify, - DockerRegistryCreds: credentials, + if _, err := pull(ctx, ic.Libpod.ImageRuntime(), imageRef, pullOptions, &label); err != nil { + return nil, err } - - return ic.Libpod.ImageRuntime().New(ctx, imageRef, options.SignaturePolicy, options.Authfile, pullOutput, &dockerRegistryOptions, image.SigningOptions{}, &label, util.PullImageMissing) + return ic.Libpod.ImageRuntime().NewFromLocal(imageRef) } // generateRunlabelCommand generates the to-be-executed command as a string diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 33060b34b..23aef9573 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -214,7 +214,7 @@ func ToDomainHistoryLayer(layer *libpodImage.History) entities.ImageHistoryLayer return l } -func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entities.ImagePullOptions) (*entities.ImagePullReport, error) { +func pull(ctx context.Context, runtime *image.Runtime, rawImage string, options entities.ImagePullOptions, label *string) (*entities.ImagePullReport, error) { var writer io.Writer if !options.Quiet { writer = os.Stderr @@ -246,7 +246,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti } if !options.AllTags { - newImage, err := ir.Libpod.ImageRuntime().New(ctx, rawImage, options.SignaturePolicy, options.Authfile, writer, &dockerRegistryOptions, image.SigningOptions{}, nil, util.PullImageAlways) + newImage, err := runtime.New(ctx, rawImage, options.SignaturePolicy, options.Authfile, writer, &dockerRegistryOptions, image.SigningOptions{}, label, util.PullImageAlways) if err != nil { return nil, err } @@ -280,7 +280,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti foundIDs := []string{} for _, tag := range tags { name := rawImage + ":" + tag - newImage, err := ir.Libpod.ImageRuntime().New(ctx, name, options.SignaturePolicy, options.Authfile, writer, &dockerRegistryOptions, image.SigningOptions{}, nil, util.PullImageAlways) + newImage, err := runtime.New(ctx, name, options.SignaturePolicy, options.Authfile, writer, &dockerRegistryOptions, image.SigningOptions{}, nil, util.PullImageAlways) if err != nil { logrus.Errorf("error pulling image %q", name) continue @@ -294,6 +294,10 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti return &entities.ImagePullReport{Images: foundIDs}, nil } +func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entities.ImagePullOptions) (*entities.ImagePullReport, error) { + return pull(ctx, ir.Libpod.ImageRuntime(), rawImage, options, nil) +} + func (ir *ImageEngine) Inspect(ctx context.Context, namesOrIDs []string, opts entities.InspectOptions) ([]*entities.ImageInspectReport, []error, error) { reports := []*entities.ImageInspectReport{} errs := []error{} diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 31ad51672..47d1c48f2 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -556,6 +556,7 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container containerConfig.Env = envs for _, volume := range containerYAML.VolumeMounts { + var readonly string hostPath, exists := volumes[volume.Name] if !exists { return nil, errors.Errorf("Volume mount %s specified for container but not configured in volumes", volume.Name) @@ -563,7 +564,10 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container if err := parse.ValidateVolumeCtrDir(volume.MountPath); err != nil { return nil, errors.Wrapf(err, "error in parsing MountPath") } - containerConfig.Volumes = append(containerConfig.Volumes, fmt.Sprintf("%s:%s", hostPath, volume.MountPath)) + if volume.ReadOnly { + readonly = ":ro" + } + containerConfig.Volumes = append(containerConfig.Volumes, fmt.Sprintf("%s:%s%s", hostPath, volume.MountPath, readonly)) } return &containerConfig, nil } diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 121cea017..5e01971cb 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -99,6 +99,12 @@ spec: hostPort: {{ .Port }} protocol: TCP workingDir: / + volumeMounts: + {{ if .VolumeMount }} + - name: {{.VolumeName}} + mountPath: {{ .VolumeMountPath }} + readonly: {{.VolumeReadOnly}} + {{ end }} {{ end }} {{ end }} {{ end }} @@ -383,12 +389,16 @@ type Ctr struct { PullPolicy string HostIP string Port string + VolumeMount bool + VolumeMountPath string + VolumeName string + VolumeReadOnly bool } // getCtr takes a list of ctrOptions and returns a Ctr with sane defaults // and the configured options func getCtr(options ...ctrOption) *Ctr { - c := Ctr{defaultCtrName, defaultCtrImage, defaultCtrCmd, defaultCtrArg, true, false, nil, nil, "", "", ""} + c := Ctr{defaultCtrName, defaultCtrImage, defaultCtrCmd, defaultCtrArg, true, false, nil, nil, "", "", "", false, "", "", false} for _, option := range options { option(&c) } @@ -448,6 +458,15 @@ func withHostIP(ip string, port string) ctrOption { } } +func withVolumeMount(mountPath string, readonly bool) ctrOption { + return func(c *Ctr) { + c.VolumeMountPath = mountPath + c.VolumeName = defaultVolName + c.VolumeReadOnly = readonly + c.VolumeMount = true + } +} + func getCtrNameInPod(pod *Pod) string { return fmt.Sprintf("%s-%s", pod.Name, defaultCtrName) } @@ -1035,4 +1054,27 @@ spec: kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).NotTo(Equal(0)) }) + + It("podman play kube test with read only volume", func() { + hostPathLocation := filepath.Join(tempdir, "file") + f, err := os.Create(hostPathLocation) + Expect(err).To(BeNil()) + f.Close() + + ctr := getCtr(withVolumeMount(hostPathLocation, true), withImage(BB)) + pod := getPod(withVolume(getVolume("File", hostPathLocation)), withCtr(ctr)) + err = generatePodKubeYaml(pod, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{.HostConfig.Binds}}'"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + + correct := fmt.Sprintf("%s:%s:%s", hostPathLocation, hostPathLocation, "ro") + Expect(inspect.OutputToString()).To(ContainSubstring(correct)) + }) }) diff --git a/test/e2e/run_passwd_test.go b/test/e2e/run_passwd_test.go index c48876dee..dfb8c72a1 100644 --- a/test/e2e/run_passwd_test.go +++ b/test/e2e/run_passwd_test.go @@ -71,4 +71,58 @@ USER 1000` Expect(session.ExitCode()).To(Equal(0)) Expect(session.OutputToString()).To(Not(ContainSubstring("passwd"))) }) + + It("podman run with no user specified does not change --group specified", func() { + session := podmanTest.Podman([]string{"run", "--read-only", BB, "mount"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.LineInOutputContains("/etc/group")).To(BeFalse()) + }) + + It("podman run group specified in container", func() { + session := podmanTest.Podman([]string{"run", "--read-only", "-u", "root:bin", BB, "mount"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.LineInOutputContains("/etc/group")).To(BeFalse()) + }) + + It("podman run non-numeric group not specified in container", func() { + session := podmanTest.Podman([]string{"run", "--read-only", "-u", "root:doesnotexist", BB, "mount"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman run numeric group specified in container", func() { + session := podmanTest.Podman([]string{"run", "--read-only", "-u", "root:11", BB, "mount"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.LineInOutputContains("/etc/group")).To(BeFalse()) + }) + + It("podman run numeric group not specified in container", func() { + session := podmanTest.Podman([]string{"run", "--read-only", "-u", "20001:20001", BB, "mount"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.LineInOutputContains("/etc/group")).To(BeTrue()) + }) + + It("podman run numeric user not specified in container modifies group", func() { + session := podmanTest.Podman([]string{"run", "--read-only", "-u", "20001", BB, "mount"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.LineInOutputContains("/etc/group")).To(BeTrue()) + }) + + It("podman run numeric group from image and no group file", func() { + SkipIfRemote() + dockerfile := `FROM alpine +RUN rm -f /etc/passwd /etc/shadow /etc/group +USER 1000` + imgName := "testimg" + podmanTest.BuildImage(dockerfile, imgName, "false") + session := podmanTest.Podman([]string{"run", "--rm", imgName, "ls", "/etc/"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Not(ContainSubstring("/etc/group"))) + }) }) diff --git a/test/e2e/run_userns_test.go b/test/e2e/run_userns_test.go index 25f8d0d15..8d860cfc3 100644 --- a/test/e2e/run_userns_test.go +++ b/test/e2e/run_userns_test.go @@ -277,6 +277,13 @@ var _ = Describe("Podman UserNS support", func() { ok, _ := session.GrepString("4998") Expect(ok).To(BeTrue()) + + session = podmanTest.Podman([]string{"run", "--rm", "--userns=container:" + ctrName, "--net=container:" + ctrName, "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + ok, _ = session.GrepString("4998") + Expect(ok).To(BeTrue()) }) It("podman --user with volume", func() { diff --git a/test/e2e/runlabel_test.go b/test/e2e/runlabel_test.go index f17b4d560..0eb679fbf 100644 --- a/test/e2e/runlabel_test.go +++ b/test/e2e/runlabel_test.go @@ -29,6 +29,8 @@ var _ = Describe("podman container runlabel", func() { ) BeforeEach(func() { + // runlabel is not supported for remote connections + SkipIfRemote() tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) @@ -46,7 +48,6 @@ var _ = Describe("podman container runlabel", func() { }) It("podman container runlabel (podman --version)", func() { - SkipIfRemote() image := "podman-runlabel-test:podman" podmanTest.BuildImage(PodmanDockerfile, image, "false") @@ -60,7 +61,6 @@ var _ = Describe("podman container runlabel", func() { }) It("podman container runlabel (ls -la)", func() { - SkipIfRemote() image := "podman-runlabel-test:ls" podmanTest.BuildImage(LsDockerfile, image, "false") @@ -72,9 +72,7 @@ var _ = Describe("podman container runlabel", func() { result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(0)) }) - It("podman container runlabel --display", func() { - SkipIfRemote() image := "podman-runlabel-test:ls" podmanTest.BuildImage(LsDockerfile, image, "false") @@ -115,7 +113,6 @@ var _ = Describe("podman container runlabel", func() { }) It("runlabel should fail with nonexist authfile", func() { - SkipIfRemote() image := "podman-runlabel-test:podman" podmanTest.BuildImage(PodmanDockerfile, image, "false") diff --git a/test/system/030-run.bats b/test/system/030-run.bats index 0b92554b8..4e518c571 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -189,9 +189,19 @@ echo $rand | 0 | $rand is "$(< $cidfile)" "$cid" "contents of cidfile == container ID" - conmon_pid=$(< $pidfile) - is "$(readlink /proc/$conmon_pid/exe)" ".*/conmon" \ - "conmon pidfile (= PID $conmon_pid) points to conmon process" + # Cross-check --conmon-pidfile against 'podman inspect' + local conmon_pid_from_file=$(< $pidfile) + run_podman inspect --format '{{.State.ConmonPid}}' $cid + local conmon_pid_from_inspect="$output" + is "$conmon_pid_from_file" "$conmon_pid_from_inspect" \ + "Conmon pid in pidfile matches what 'podman inspect' claims" + + # /proc/PID/exe should be a symlink to a conmon executable + # FIXME: 'echo' and 'ls' are to help debug #7580, a CI flake + echo "conmon pid = $conmon_pid_from_file" + ls -l /proc/$conmon_pid_from_file + is "$(readlink /proc/$conmon_pid_from_file/exe)" ".*/conmon" \ + "conmon pidfile (= PID $conmon_pid_from_file) points to conmon process" # All OK. Kill container. run_podman rm -f $cid @@ -204,7 +214,7 @@ echo $rand | 0 | $rand } @test "podman run docker-archive" { - skip_if_remote "FIXME: pending #7116" + skip_if_remote "podman-remote does not support docker-archive (#7116)" # Create an image that, when run, outputs a random magic string expect=$(random_string 20) diff --git a/test/system/120-load.bats b/test/system/120-load.bats index 86b396c4a..d7aa16d95 100644 --- a/test/system/120-load.bats +++ b/test/system/120-load.bats @@ -27,25 +27,43 @@ verify_iid_and_name() { } @test "podman save to pipe and load" { - get_iid_and_name + # Generate a random name and tag (must be lower-case) + local random_name=x$(random_string 12 | tr A-Z a-z) + local random_tag=t$(random_string 7 | tr A-Z a-z) + local fqin=localhost/$random_name:$random_tag + run_podman tag $IMAGE $fqin + + archive=$PODMAN_TMPDIR/myimage-$(random_string 8).tar # We can't use run_podman because that uses the BATS 'run' function # which redirects stdout and stderr. Here we need to guarantee # that podman's stdout is a pipe, not any other form of redirection - $PODMAN save --format oci-archive $IMAGE | cat >$archive + $PODMAN save --format oci-archive $fqin | cat >$archive if [ "$status" -ne 0 ]; then die "Command failed: podman save ... | cat" fi # Make sure we can reload it - # FIXME: when/if 7337 gets fixed, add a random tag instead of rmi'ing - # FIXME: when/if 7371 gets fixed, use verify_iid_and_name() - run_podman rmi $iid + run_podman rmi $fqin run_podman load -i $archive - # FIXME: cannot compare IID, see #7371 - run_podman images -a --format '{{.Repository}}:{{.Tag}}' - is "$output" "$IMAGE" "image preserves name across save/load" + # FIXME: cannot compare IID, see #7371, so we check only the tag + run_podman images $fqin --format '{{.Repository}}:{{.Tag}}' + is "$output" "$fqin" "image preserves name across save/load" + + # FIXME: when/if 7337 gets fixed, load with a new tag + if false; then + local new_name=x$(random_string 14 | tr A-Z a-z) + local new_tag=t$(random_string 6 | tr A-Z a-z) + run_podman rmi $fqin + fqin=localhost/$new_name:$new_tag + run_podman load -i $archive $fqin + run_podman images $fqin --format '{{.Repository}}:{{.Tag}}' + is "$output" "$fqin" "image can be loaded with new name:tag" + fi + + # Clean up + run_podman rmi $fqin } |