mirror of https://github.com/containers/podman.git
libpod: Move functions related to /etc bind mounts to container_internal_common.go
[NO NEW TESTS NEEDED] Signed-off-by: Doug Rabson <dfr@rabson.org>
This commit is contained in:
parent
b3989be768
commit
eab4291d99
|
|
@ -11,6 +11,7 @@ import (
|
|||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
|
@ -24,8 +25,12 @@ import (
|
|||
"github.com/containers/buildah/pkg/chrootuser"
|
||||
"github.com/containers/buildah/pkg/overlay"
|
||||
butil "github.com/containers/buildah/util"
|
||||
"github.com/containers/common/libnetwork/etchosts"
|
||||
"github.com/containers/common/libnetwork/resolvconf"
|
||||
"github.com/containers/common/libnetwork/types"
|
||||
"github.com/containers/common/pkg/apparmor"
|
||||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/common/pkg/subscriptions"
|
||||
cutil "github.com/containers/common/pkg/util"
|
||||
is "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/podman/v4/libpod/define"
|
||||
|
|
@ -39,6 +44,7 @@ import (
|
|||
"github.com/containers/podman/v4/version"
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/lockfile"
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
runcuser "github.com/opencontainers/runc/libcontainer/user"
|
||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||
|
|
@ -1595,3 +1601,953 @@ func (c *Container) getRootNetNsDepCtr() (depCtr *Container, err error) {
|
|||
}
|
||||
return depCtr, nil
|
||||
}
|
||||
|
||||
// Ensure standard bind mounts are mounted into all root directories (including chroot directories)
|
||||
func (c *Container) mountIntoRootDirs(mountName string, mountPath string) error {
|
||||
c.state.BindMounts[mountName] = mountPath
|
||||
|
||||
for _, chrootDir := range c.config.ChrootDirs {
|
||||
c.state.BindMounts[filepath.Join(chrootDir, mountName)] = mountPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make standard bind mounts to include in the container
|
||||
func (c *Container) makeBindMounts() error {
|
||||
if err := os.Chown(c.state.RunDir, c.RootUID(), c.RootGID()); err != nil {
|
||||
return fmt.Errorf("cannot chown run directory: %w", err)
|
||||
}
|
||||
|
||||
if c.state.BindMounts == nil {
|
||||
c.state.BindMounts = make(map[string]string)
|
||||
}
|
||||
netDisabled, err := c.NetworkDisabled()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !netDisabled {
|
||||
// If /etc/resolv.conf and /etc/hosts exist, delete them so we
|
||||
// will recreate. Only do this if we aren't sharing them with
|
||||
// another container.
|
||||
if c.config.NetNsCtr == "" {
|
||||
if resolvePath, ok := c.state.BindMounts["/etc/resolv.conf"]; ok {
|
||||
if err := os.Remove(resolvePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
delete(c.state.BindMounts, "/etc/resolv.conf")
|
||||
}
|
||||
if hostsPath, ok := c.state.BindMounts["/etc/hosts"]; ok {
|
||||
if err := os.Remove(hostsPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
delete(c.state.BindMounts, "/etc/hosts")
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.NetNsCtr != "" && (!c.config.UseImageResolvConf || !c.config.UseImageHosts) {
|
||||
// We share a net namespace.
|
||||
// We want /etc/resolv.conf and /etc/hosts from the
|
||||
// other container. Unless we're not creating both of
|
||||
// them.
|
||||
depCtr, err := c.getRootNetNsDepCtr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching network namespace dependency container for container %s: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
// We need that container's bind mounts
|
||||
bindMounts, err := depCtr.BindMounts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching bind mounts from dependency %s of container %s: %w", depCtr.ID(), c.ID(), err)
|
||||
}
|
||||
|
||||
// The other container may not have a resolv.conf or /etc/hosts
|
||||
// If it doesn't, don't copy them
|
||||
resolvPath, exists := bindMounts["/etc/resolv.conf"]
|
||||
if !c.config.UseImageResolvConf && exists {
|
||||
err := c.mountIntoRootDirs("/etc/resolv.conf", resolvPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// check if dependency container has an /etc/hosts file.
|
||||
// It may not have one, so only use it if it does.
|
||||
hostsPath, exists := bindMounts[config.DefaultHostsFile]
|
||||
if !c.config.UseImageHosts && exists {
|
||||
// we cannot use the dependency container lock due ABBA deadlocks in cleanup()
|
||||
lock, err := lockfile.GetLockfile(hostsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lock hosts file: %w", err)
|
||||
}
|
||||
lock.Lock()
|
||||
|
||||
// add the newly added container to the hosts file
|
||||
// we always use 127.0.0.1 as ip since they have the same netns
|
||||
err = etchosts.Add(hostsPath, getLocalhostHostEntry(c))
|
||||
lock.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s which depends on container %s: %w", c.ID(), depCtr.ID(), err)
|
||||
}
|
||||
|
||||
// finally, save it in the new container
|
||||
err = c.mountIntoRootDirs(config.DefaultHostsFile, hostsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasCurrentUserMapped(c) {
|
||||
if err := makeAccessible(resolvPath, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := makeAccessible(hostsPath, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !c.config.UseImageResolvConf {
|
||||
if err := c.generateResolvConf(); err != nil {
|
||||
return fmt.Errorf("error creating resolv.conf for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if !c.config.UseImageHosts {
|
||||
if err := c.createHosts(); err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.state.BindMounts["/etc/hosts"] != "" {
|
||||
if err := c.relabel(c.state.BindMounts["/etc/hosts"], c.config.MountLabel, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.state.BindMounts["/etc/resolv.conf"] != "" {
|
||||
if err := c.relabel(c.state.BindMounts["/etc/resolv.conf"], c.config.MountLabel, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !c.config.UseImageHosts && c.state.BindMounts["/etc/hosts"] == "" {
|
||||
if err := c.createHosts(); err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.ShmDir != "" {
|
||||
// If ShmDir has a value SHM is always added when we mount the container
|
||||
c.state.BindMounts["/dev/shm"] = c.config.ShmDir
|
||||
}
|
||||
|
||||
if c.config.Passwd == nil || *c.config.Passwd {
|
||||
newPasswd, newGroup, err := c.generatePasswdAndGroup()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating temporary passwd file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
if newPasswd != "" {
|
||||
// Make /etc/passwd
|
||||
// If it already exists, delete so we can recreate
|
||||
delete(c.state.BindMounts, "/etc/passwd")
|
||||
c.state.BindMounts["/etc/passwd"] = newPasswd
|
||||
}
|
||||
if newGroup != "" {
|
||||
// Make /etc/group
|
||||
// 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
|
||||
if _, ok := c.state.BindMounts["/etc/hostname"]; !ok {
|
||||
hostnamePath, err := c.writeStringToRundir("hostname", c.Hostname())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating hostname file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
c.state.BindMounts["/etc/hostname"] = hostnamePath
|
||||
}
|
||||
|
||||
// Make /etc/localtime
|
||||
ctrTimezone := c.Timezone()
|
||||
if ctrTimezone != "" {
|
||||
// validate the format of the timezone specified if it's not "local"
|
||||
if ctrTimezone != "local" {
|
||||
_, err = time.LoadLocation(ctrTimezone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
if _, ok := c.state.BindMounts["/etc/localtime"]; !ok {
|
||||
var zonePath string
|
||||
if ctrTimezone == "local" {
|
||||
zonePath, err = filepath.EvalSymlinks("/etc/localtime")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding local timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
} else {
|
||||
zone := filepath.Join("/usr/share/zoneinfo", ctrTimezone)
|
||||
zonePath, err = filepath.EvalSymlinks(zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
localtimePath, err := c.copyTimezoneFile(zonePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
c.state.BindMounts["/etc/localtime"] = localtimePath
|
||||
}
|
||||
}
|
||||
|
||||
_, hasRunContainerenv := c.state.BindMounts["/run/.containerenv"]
|
||||
if !hasRunContainerenv {
|
||||
// check in the spec mounts
|
||||
for _, m := range c.config.Spec.Mounts {
|
||||
if m.Destination == "/run/.containerenv" || m.Destination == "/run" {
|
||||
hasRunContainerenv = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make .containerenv if it does not exist
|
||||
if !hasRunContainerenv {
|
||||
containerenv := c.runtime.graphRootMountedFlag(c.config.Spec.Mounts)
|
||||
isRootless := 0
|
||||
if rootless.IsRootless() {
|
||||
isRootless = 1
|
||||
}
|
||||
imageID, imageName := c.Image()
|
||||
|
||||
if c.Privileged() {
|
||||
// Populate the .containerenv with container information
|
||||
containerenv = fmt.Sprintf(`engine="podman-%s"
|
||||
name=%q
|
||||
id=%q
|
||||
image=%q
|
||||
imageid=%q
|
||||
rootless=%d
|
||||
%s`, version.Version.String(), c.Name(), c.ID(), imageName, imageID, isRootless, containerenv)
|
||||
}
|
||||
containerenvPath, err := c.writeStringToRundir(".containerenv", containerenv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating containerenv file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
c.state.BindMounts["/run/.containerenv"] = containerenvPath
|
||||
}
|
||||
|
||||
// Add Subscription Mounts
|
||||
subscriptionMounts := subscriptions.MountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.Containers.DefaultMountsFile, c.state.Mountpoint, c.RootUID(), c.RootGID(), rootless.IsRootless(), false)
|
||||
for _, mount := range subscriptionMounts {
|
||||
if _, ok := c.state.BindMounts[mount.Destination]; !ok {
|
||||
c.state.BindMounts[mount.Destination] = mount.Source
|
||||
}
|
||||
}
|
||||
|
||||
// Secrets are mounted by getting the secret data from the secrets manager,
|
||||
// copying the data into the container's static dir,
|
||||
// then mounting the copied dir into /run/secrets.
|
||||
// The secrets mounting must come after subscription mounts, since subscription mounts
|
||||
// creates the /run/secrets dir in the container where we mount as well.
|
||||
if len(c.Secrets()) > 0 {
|
||||
// create /run/secrets if subscriptions did not create
|
||||
if err := c.createSecretMountDir(); err != nil {
|
||||
return fmt.Errorf("error creating secrets mount: %w", err)
|
||||
}
|
||||
for _, secret := range c.Secrets() {
|
||||
secretFileName := secret.Name
|
||||
base := "/run/secrets"
|
||||
if secret.Target != "" {
|
||||
secretFileName = secret.Target
|
||||
// If absolute path for target given remove base.
|
||||
if filepath.IsAbs(secretFileName) {
|
||||
base = ""
|
||||
}
|
||||
}
|
||||
src := filepath.Join(c.config.SecretsPath, secret.Name)
|
||||
dest := filepath.Join(base, secretFileName)
|
||||
c.state.BindMounts[dest] = src
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateResolvConf generates a containers resolv.conf
|
||||
func (c *Container) generateResolvConf() error {
|
||||
var (
|
||||
networkNameServers []string
|
||||
networkSearchDomains []string
|
||||
)
|
||||
|
||||
netStatus := c.getNetworkStatus()
|
||||
for _, status := range netStatus {
|
||||
if status.DNSServerIPs != nil {
|
||||
for _, nsIP := range status.DNSServerIPs {
|
||||
networkNameServers = append(networkNameServers, nsIP.String())
|
||||
}
|
||||
logrus.Debugf("Adding nameserver(s) from network status of '%q'", status.DNSServerIPs)
|
||||
}
|
||||
if status.DNSSearchDomains != nil {
|
||||
networkSearchDomains = append(networkSearchDomains, status.DNSSearchDomains...)
|
||||
logrus.Debugf("Adding search domain(s) from network status of '%q'", status.DNSSearchDomains)
|
||||
}
|
||||
}
|
||||
|
||||
ipv6, err := c.checkForIPv6(netStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nameservers := make([]string, 0, len(c.runtime.config.Containers.DNSServers)+len(c.config.DNSServer))
|
||||
nameservers = append(nameservers, c.runtime.config.Containers.DNSServers...)
|
||||
for _, ip := range c.config.DNSServer {
|
||||
nameservers = append(nameservers, ip.String())
|
||||
}
|
||||
// If the user provided dns, it trumps all; then dns masq; then resolv.conf
|
||||
var search []string
|
||||
keepHostServers := false
|
||||
if len(nameservers) == 0 {
|
||||
keepHostServers = true
|
||||
// first add the nameservers from the networks status
|
||||
nameservers = networkNameServers
|
||||
// when we add network dns server we also have to add the search domains
|
||||
search = networkSearchDomains
|
||||
// slirp4netns has a built in DNS forwarder.
|
||||
if c.config.NetMode.IsSlirp4netns() {
|
||||
slirp4netnsDNS, err := GetSlirp4netnsDNS(c.slirp4netnsSubnet)
|
||||
if err != nil {
|
||||
logrus.Warn("Failed to determine Slirp4netns DNS: ", err.Error())
|
||||
} else {
|
||||
nameservers = append(nameservers, slirp4netnsDNS.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.config.DNSSearch) > 0 || len(c.runtime.config.Containers.DNSSearches) > 0 {
|
||||
customSearch := make([]string, 0, len(c.config.DNSSearch)+len(c.runtime.config.Containers.DNSSearches))
|
||||
customSearch = append(customSearch, c.runtime.config.Containers.DNSSearches...)
|
||||
customSearch = append(customSearch, c.config.DNSSearch...)
|
||||
search = customSearch
|
||||
}
|
||||
|
||||
options := make([]string, 0, len(c.config.DNSOption)+len(c.runtime.config.Containers.DNSOptions))
|
||||
options = append(options, c.runtime.config.Containers.DNSOptions...)
|
||||
options = append(options, c.config.DNSOption...)
|
||||
|
||||
destPath := filepath.Join(c.state.RunDir, "resolv.conf")
|
||||
|
||||
if err := resolvconf.New(&resolvconf.Params{
|
||||
IPv6Enabled: ipv6,
|
||||
KeepHostServers: keepHostServers,
|
||||
Nameservers: nameservers,
|
||||
Namespaces: c.config.Spec.Linux.Namespaces,
|
||||
Options: options,
|
||||
Path: destPath,
|
||||
Searches: search,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error building resolv.conf for container %s: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return c.bindMountRootFile(destPath, resolvconf.DefaultResolvConf)
|
||||
}
|
||||
|
||||
// Check if a container uses IPv6.
|
||||
func (c *Container) checkForIPv6(netStatus map[string]types.StatusBlock) (bool, error) {
|
||||
for _, status := range netStatus {
|
||||
for _, netInt := range status.Interfaces {
|
||||
for _, netAddress := range netInt.Subnets {
|
||||
// Note: only using To16() does not work since it also returns a valid ip for ipv4
|
||||
if netAddress.IPNet.IP.To4() == nil && netAddress.IPNet.IP.To16() != nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.NetMode.IsSlirp4netns() {
|
||||
ctrNetworkSlipOpts := []string{}
|
||||
if c.config.NetworkOptions != nil {
|
||||
ctrNetworkSlipOpts = append(ctrNetworkSlipOpts, c.config.NetworkOptions["slirp4netns"]...)
|
||||
}
|
||||
slirpOpts, err := parseSlirp4netnsNetworkOptions(c.runtime, ctrNetworkSlipOpts)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return slirpOpts.enableIPv6, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Add a new nameserver to the container's resolv.conf, ensuring that it is the
|
||||
// first nameserver present.
|
||||
// Usable only with running containers.
|
||||
func (c *Container) addNameserver(ips []string) error {
|
||||
// Take no action if container is not running.
|
||||
if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do we have a resolv.conf at all?
|
||||
path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := resolvconf.Add(path, ips); err != nil {
|
||||
return fmt.Errorf("adding new nameserver to container %s resolv.conf: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove an entry from the existing resolv.conf of the container.
|
||||
// Usable only with running containers.
|
||||
func (c *Container) removeNameserver(ips []string) error {
|
||||
// Take no action if container is not running.
|
||||
if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do we have a resolv.conf at all?
|
||||
path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := resolvconf.Remove(path, ips); err != nil {
|
||||
return fmt.Errorf("removing nameservers from container %s resolv.conf: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalhostHostEntry(c *Container) etchosts.HostEntries {
|
||||
return etchosts.HostEntries{{IP: "127.0.0.1", Names: []string{c.Hostname(), c.config.Name}}}
|
||||
}
|
||||
|
||||
// getHostsEntries returns the container ip host entries for the correct netmode
|
||||
func (c *Container) getHostsEntries() (etchosts.HostEntries, error) {
|
||||
var entries etchosts.HostEntries
|
||||
names := []string{c.Hostname(), c.config.Name}
|
||||
switch {
|
||||
case c.config.NetMode.IsBridge():
|
||||
entries = etchosts.GetNetworkHostEntries(c.state.NetworkStatus, names...)
|
||||
case c.config.NetMode.IsSlirp4netns():
|
||||
ip, err := GetSlirp4netnsIP(c.slirp4netnsSubnet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = etchosts.HostEntries{{IP: ip.String(), Names: names}}
|
||||
default:
|
||||
// check for net=none
|
||||
if !c.config.CreateNetNS {
|
||||
for _, ns := range c.config.Spec.Linux.Namespaces {
|
||||
if ns.Type == spec.NetworkNamespace {
|
||||
if ns.Path == "" {
|
||||
entries = etchosts.HostEntries{{IP: "127.0.0.1", Names: names}}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (c *Container) createHosts() error {
|
||||
var containerIPsEntries etchosts.HostEntries
|
||||
var err error
|
||||
// if we configure the netns after the container create we should not add
|
||||
// the hosts here since we have no information about the actual ips
|
||||
// instead we will add them in c.completeNetworkSetup()
|
||||
if !c.config.PostConfigureNetNS {
|
||||
containerIPsEntries, err = c.getHostsEntries()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get container ip host entries: %w", err)
|
||||
}
|
||||
}
|
||||
baseHostFile, err := etchosts.GetBaseHostFile(c.runtime.config.Containers.BaseHostsFile, c.state.Mountpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile := filepath.Join(c.state.RunDir, "hosts")
|
||||
err = etchosts.New(&etchosts.Params{
|
||||
BaseFile: baseHostFile,
|
||||
ExtraHosts: c.config.HostAdd,
|
||||
ContainerIPs: containerIPsEntries,
|
||||
HostContainersInternalIP: etchosts.GetHostContainersInternalIP(c.runtime.config, c.state.NetworkStatus, c.runtime.network),
|
||||
TargetFile: targetFile,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.bindMountRootFile(targetFile, config.DefaultHostsFile)
|
||||
}
|
||||
|
||||
// bindMountRootFile will chown and relabel the source file to make it usable in the container.
|
||||
// It will also add the path to the container bind mount map.
|
||||
// source is the path on the host, dest is the path in the container.
|
||||
func (c *Container) bindMountRootFile(source, dest string) error {
|
||||
if err := os.Chown(source, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := label.Relabel(source, c.MountLabel(), false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.mountIntoRootDirs(dest, source)
|
||||
}
|
||||
|
||||
// generateGroupEntry generates an entry or entries into /etc/group as
|
||||
// required by container configuration.
|
||||
// Generally 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, fmt.Errorf("failed to get current group: %w", err)
|
||||
}
|
||||
|
||||
// Look up 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
|
||||
}
|
||||
|
||||
// Look up 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, fmt.Errorf("failed to get current user to make group entry: %w", err)
|
||||
}
|
||||
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, error) {
|
||||
if c.config.User == "" {
|
||||
return "", 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 "", nil //nolint: nilerr
|
||||
}
|
||||
|
||||
if addedGID != 0 && addedGID == int(gid) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Check if the group already exists
|
||||
_, err = lookup.GetGroup(c.state.Mountpoint, group)
|
||||
if err != runcuser.ErrNoGroupEntries {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d:x:%d:%s\n", gid, gid, splitUser[0]), 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).
|
||||
// 3. The user specified additional host user accounts to add the the /etc/passwd file
|
||||
//
|
||||
// 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
|
||||
for _, userid := range c.config.HostUsers {
|
||||
// Look up User on host
|
||||
u, err := util.LookupUser(userid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
entry, err := c.userPasswdEntry(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwdString += entry
|
||||
}
|
||||
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.
|
||||
// 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 "", 0, 0, nil
|
||||
}
|
||||
|
||||
u, err := user.LookupId(strconv.Itoa(uid))
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
pwd, err := c.userPasswdEntry(u)
|
||||
if err != nil {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
|
||||
return pwd, uid, rootless.GetRootlessGID(), nil
|
||||
}
|
||||
|
||||
func (c *Container) userPasswdEntry(u *user.User) (string, error) {
|
||||
// Look up the user to see if it exists in the container image.
|
||||
_, err := lookup.GetUser(c.state.Mountpoint, u.Username)
|
||||
if err != runcuser.ErrNoPasswdEntries {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Look up the UID to see if it exists in the container image.
|
||||
_, err = lookup.GetUser(c.state.Mountpoint, u.Uid)
|
||||
if err != runcuser.ErrNoPasswdEntries {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If the user's actual home directory exists, or was mounted in - use
|
||||
// that.
|
||||
homeDir := c.WorkingDir()
|
||||
hDir := u.HomeDir
|
||||
for hDir != "/" {
|
||||
if MountExists(c.config.Spec.Mounts, hDir) {
|
||||
homeDir = u.HomeDir
|
||||
break
|
||||
}
|
||||
hDir = filepath.Dir(hDir)
|
||||
}
|
||||
if homeDir != u.HomeDir {
|
||||
for _, hDir := range c.UserVolumes() {
|
||||
if hDir == u.HomeDir {
|
||||
homeDir = u.HomeDir
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set HOME environment if not already set
|
||||
hasHomeSet := false
|
||||
for _, s := range c.config.Spec.Process.Env {
|
||||
if strings.HasPrefix(s, "HOME=") {
|
||||
hasHomeSet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasHomeSet {
|
||||
c.config.Spec.Process.Env = append(c.config.Spec.Process.Env, fmt.Sprintf("HOME=%s", homeDir))
|
||||
}
|
||||
if c.config.PasswdEntry != "" {
|
||||
return c.passwdEntry(u.Username, u.Uid, u.Gid, u.Name, homeDir), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Name, homeDir), nil
|
||||
}
|
||||
|
||||
// generateUserPasswdEntry generates an /etc/passwd entry for the container user
|
||||
// to run in the container.
|
||||
// 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, error) {
|
||||
var (
|
||||
groupspec string
|
||||
gid int
|
||||
)
|
||||
if c.config.User == "" {
|
||||
return "", nil
|
||||
}
|
||||
splitSpec := strings.SplitN(c.config.User, ":", 2)
|
||||
userspec := splitSpec[0]
|
||||
if len(splitSpec) > 1 {
|
||||
groupspec = splitSpec[1]
|
||||
}
|
||||
// If a non numeric User, then don't generate passwd
|
||||
uid, err := strconv.ParseUint(userspec, 10, 32)
|
||||
if err != nil {
|
||||
return "", nil //nolint: nilerr
|
||||
}
|
||||
|
||||
if addedUID != 0 && int(uid) == addedUID {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Look up the user to see if it exists in the container image
|
||||
_, err = lookup.GetUser(c.state.Mountpoint, userspec)
|
||||
if err != runcuser.ErrNoPasswdEntries {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if groupspec != "" {
|
||||
ugid, err := strconv.ParseUint(groupspec, 10, 32)
|
||||
if err == nil {
|
||||
gid = int(ugid)
|
||||
} else {
|
||||
group, err := lookup.GetGroup(c.state.Mountpoint, groupspec)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get gid %s from group file: %w", groupspec, err)
|
||||
}
|
||||
gid = group.Gid
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.PasswdEntry != "" {
|
||||
entry := c.passwdEntry(fmt.Sprintf("%d", uid), fmt.Sprintf("%d", uid), fmt.Sprintf("%d", gid), "container user", c.WorkingDir())
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d:*:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), nil
|
||||
}
|
||||
|
||||
func (c *Container) passwdEntry(username string, uid, gid, name, homeDir string) string {
|
||||
s := c.config.PasswdEntry
|
||||
s = strings.ReplaceAll(s, "$USERNAME", username)
|
||||
s = strings.ReplaceAll(s, "$UID", uid)
|
||||
s = strings.ReplaceAll(s, "$GID", gid)
|
||||
s = strings.ReplaceAll(s, "$NAME", name)
|
||||
s = strings.ReplaceAll(s, "$HOME", homeDir)
|
||||
return s + "\n"
|
||||
}
|
||||
|
||||
// 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 or the user specified HostsUsers
|
||||
// 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 == "" &&
|
||||
len(c.config.HostUsers) == 0 {
|
||||
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") {
|
||||
needPasswd = false
|
||||
}
|
||||
if MountExists(c.config.Spec.Mounts, "/etc/group") {
|
||||
needGroup = false
|
||||
}
|
||||
|
||||
// Next, check if we already made the files. If we didn't, 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 needGroup {
|
||||
groupPath := filepath.Join(c.config.StaticDir, "group")
|
||||
if _, err := os.Stat(groupPath); err == nil {
|
||||
needGroup = false
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't need a /etc/passwd or /etc/group at this point we can
|
||||
// just return.
|
||||
if !needPasswd && !needGroup {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
passwdPath := ""
|
||||
groupPath := ""
|
||||
|
||||
ro := c.IsReadOnly()
|
||||
|
||||
if needPasswd {
|
||||
passwdEntry, err := c.generatePasswdEntry()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
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 "", "", fmt.Errorf("error creating path to container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
orig, err := ioutil.ReadFile(originPasswdFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", "", err
|
||||
}
|
||||
passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+passwdEntry)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create temporary passwd file: %w", err)
|
||||
}
|
||||
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 "", "", fmt.Errorf("error looking up location of container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(containerPasswd, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(passwdEntry); err != nil {
|
||||
return "", "", fmt.Errorf("unable to append to container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
default:
|
||||
logrus.Debugf("Not modifying container %s /etc/passwd", c.ID())
|
||||
}
|
||||
}
|
||||
if needGroup {
|
||||
groupEntry, err := c.generateGroupEntry()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
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 "", "", fmt.Errorf("error creating path to container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
orig, err := ioutil.ReadFile(originGroupFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", "", err
|
||||
}
|
||||
groupFile, err := c.writeStringToStaticDir("group", string(orig)+groupEntry)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create temporary group file: %w", err)
|
||||
}
|
||||
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 "", "", fmt.Errorf("error looking up location of container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(containerGroup, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(groupEntry); err != nil {
|
||||
return "", "", fmt.Errorf("unable to append to container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
default:
|
||||
logrus.Debugf("Not modifying container %s /etc/group", c.ID())
|
||||
}
|
||||
}
|
||||
|
||||
return passwdPath, groupPath, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,33 +7,20 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containers/buildah/pkg/overlay"
|
||||
"github.com/containers/common/libnetwork/etchosts"
|
||||
"github.com/containers/common/libnetwork/resolvconf"
|
||||
"github.com/containers/common/libnetwork/types"
|
||||
"github.com/containers/common/pkg/chown"
|
||||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/common/pkg/subscriptions"
|
||||
"github.com/containers/common/pkg/umask"
|
||||
"github.com/containers/podman/v4/libpod/define"
|
||||
"github.com/containers/podman/v4/pkg/lookup"
|
||||
"github.com/containers/podman/v4/pkg/rootless"
|
||||
"github.com/containers/podman/v4/pkg/util"
|
||||
"github.com/containers/podman/v4/version"
|
||||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/lockfile"
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
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"
|
||||
|
|
@ -190,917 +177,6 @@ func (c *Container) addNetworkContainer(g *generate.Generator, ctr string) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// Ensure standard bind mounts are mounted into all root directories (including chroot directories)
|
||||
func (c *Container) mountIntoRootDirs(mountName string, mountPath string) error {
|
||||
c.state.BindMounts[mountName] = mountPath
|
||||
|
||||
for _, chrootDir := range c.config.ChrootDirs {
|
||||
c.state.BindMounts[filepath.Join(chrootDir, mountName)] = mountPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make standard bind mounts to include in the container
|
||||
func (c *Container) makeBindMounts() error {
|
||||
if err := os.Chown(c.state.RunDir, c.RootUID(), c.RootGID()); err != nil {
|
||||
return fmt.Errorf("cannot chown run directory: %w", err)
|
||||
}
|
||||
|
||||
if c.state.BindMounts == nil {
|
||||
c.state.BindMounts = make(map[string]string)
|
||||
}
|
||||
netDisabled, err := c.NetworkDisabled()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !netDisabled {
|
||||
// If /etc/resolv.conf and /etc/hosts exist, delete them so we
|
||||
// will recreate. Only do this if we aren't sharing them with
|
||||
// another container.
|
||||
if c.config.NetNsCtr == "" {
|
||||
if resolvePath, ok := c.state.BindMounts["/etc/resolv.conf"]; ok {
|
||||
if err := os.Remove(resolvePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
delete(c.state.BindMounts, "/etc/resolv.conf")
|
||||
}
|
||||
if hostsPath, ok := c.state.BindMounts["/etc/hosts"]; ok {
|
||||
if err := os.Remove(hostsPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
delete(c.state.BindMounts, "/etc/hosts")
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.NetNsCtr != "" && (!c.config.UseImageResolvConf || !c.config.UseImageHosts) {
|
||||
// We share a net namespace.
|
||||
// We want /etc/resolv.conf and /etc/hosts from the
|
||||
// other container. Unless we're not creating both of
|
||||
// them.
|
||||
depCtr, err := c.getRootNetNsDepCtr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching network namespace dependency container for container %s: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
// We need that container's bind mounts
|
||||
bindMounts, err := depCtr.BindMounts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching bind mounts from dependency %s of container %s: %w", depCtr.ID(), c.ID(), err)
|
||||
}
|
||||
|
||||
// The other container may not have a resolv.conf or /etc/hosts
|
||||
// If it doesn't, don't copy them
|
||||
resolvPath, exists := bindMounts["/etc/resolv.conf"]
|
||||
if !c.config.UseImageResolvConf && exists {
|
||||
err := c.mountIntoRootDirs("/etc/resolv.conf", resolvPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// check if dependency container has an /etc/hosts file.
|
||||
// It may not have one, so only use it if it does.
|
||||
hostsPath, exists := bindMounts[config.DefaultHostsFile]
|
||||
if !c.config.UseImageHosts && exists {
|
||||
// we cannot use the dependency container lock due ABBA deadlocks in cleanup()
|
||||
lock, err := lockfile.GetLockfile(hostsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lock hosts file: %w", err)
|
||||
}
|
||||
lock.Lock()
|
||||
|
||||
// add the newly added container to the hosts file
|
||||
// we always use 127.0.0.1 as ip since they have the same netns
|
||||
err = etchosts.Add(hostsPath, getLocalhostHostEntry(c))
|
||||
lock.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s which depends on container %s: %w", c.ID(), depCtr.ID(), err)
|
||||
}
|
||||
|
||||
// finally, save it in the new container
|
||||
err = c.mountIntoRootDirs(config.DefaultHostsFile, hostsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasCurrentUserMapped(c) {
|
||||
if err := makeAccessible(resolvPath, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := makeAccessible(hostsPath, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !c.config.UseImageResolvConf {
|
||||
if err := c.generateResolvConf(); err != nil {
|
||||
return fmt.Errorf("error creating resolv.conf for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if !c.config.UseImageHosts {
|
||||
if err := c.createHosts(); err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.state.BindMounts["/etc/hosts"] != "" {
|
||||
if err := c.relabel(c.state.BindMounts["/etc/hosts"], c.config.MountLabel, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.state.BindMounts["/etc/resolv.conf"] != "" {
|
||||
if err := c.relabel(c.state.BindMounts["/etc/resolv.conf"], c.config.MountLabel, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := c.createHosts(); err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.Passwd == nil || *c.config.Passwd {
|
||||
newPasswd, newGroup, err := c.generatePasswdAndGroup()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating temporary passwd file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
if newPasswd != "" {
|
||||
// Make /etc/passwd
|
||||
// If it already exists, delete so we can recreate
|
||||
delete(c.state.BindMounts, "/etc/passwd")
|
||||
c.state.BindMounts["/etc/passwd"] = newPasswd
|
||||
}
|
||||
if newGroup != "" {
|
||||
// Make /etc/group
|
||||
// If it already exists, delete so we can recreate
|
||||
delete(c.state.BindMounts, "/etc/group")
|
||||
c.state.BindMounts["/etc/group"] = newGroup
|
||||
}
|
||||
}
|
||||
|
||||
// Make /etc/localtime
|
||||
ctrTimezone := c.Timezone()
|
||||
if ctrTimezone != "" {
|
||||
// validate the format of the timezone specified if it's not "local"
|
||||
if ctrTimezone != "local" {
|
||||
_, err = time.LoadLocation(ctrTimezone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
if _, ok := c.state.BindMounts["/etc/localtime"]; !ok {
|
||||
var zonePath string
|
||||
if ctrTimezone == "local" {
|
||||
zonePath, err = filepath.EvalSymlinks("/etc/localtime")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding local timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
} else {
|
||||
zone := filepath.Join("/usr/share/zoneinfo", ctrTimezone)
|
||||
zonePath, err = filepath.EvalSymlinks(zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
localtimePath, err := c.copyTimezoneFile(zonePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
c.state.BindMounts["/etc/localtime"] = localtimePath
|
||||
}
|
||||
}
|
||||
|
||||
// Make .containerenv if it does not exist
|
||||
if _, ok := c.state.BindMounts["/run/.containerenv"]; !ok {
|
||||
containerenv := c.runtime.graphRootMountedFlag(c.config.Spec.Mounts)
|
||||
isRootless := 0
|
||||
if rootless.IsRootless() {
|
||||
isRootless = 1
|
||||
}
|
||||
imageID, imageName := c.Image()
|
||||
|
||||
if c.Privileged() {
|
||||
// Populate the .containerenv with container information
|
||||
containerenv = fmt.Sprintf(`engine="podman-%s"
|
||||
name=%q
|
||||
id=%q
|
||||
image=%q
|
||||
imageid=%q
|
||||
rootless=%d
|
||||
%s`, version.Version.String(), c.Name(), c.ID(), imageName, imageID, isRootless, containerenv)
|
||||
}
|
||||
containerenvPath, err := c.writeStringToRundir(".containerenv", containerenv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating containerenv file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
c.state.BindMounts["/run/.containerenv"] = containerenvPath
|
||||
}
|
||||
|
||||
// Add Subscription Mounts
|
||||
subscriptionMounts := subscriptions.MountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.Containers.DefaultMountsFile, c.state.Mountpoint, c.RootUID(), c.RootGID(), rootless.IsRootless(), false)
|
||||
for _, mount := range subscriptionMounts {
|
||||
if _, ok := c.state.BindMounts[mount.Destination]; !ok {
|
||||
c.state.BindMounts[mount.Destination] = mount.Source
|
||||
}
|
||||
}
|
||||
|
||||
// Secrets are mounted by getting the secret data from the secrets manager,
|
||||
// copying the data into the container's static dir,
|
||||
// then mounting the copied dir into /run/secrets.
|
||||
// The secrets mounting must come after subscription mounts, since subscription mounts
|
||||
// creates the /run/secrets dir in the container where we mount as well.
|
||||
if len(c.Secrets()) > 0 {
|
||||
// create /run/secrets if subscriptions did not create
|
||||
if err := c.createSecretMountDir(); err != nil {
|
||||
return fmt.Errorf("error creating secrets mount: %w", err)
|
||||
}
|
||||
for _, secret := range c.Secrets() {
|
||||
secretFileName := secret.Name
|
||||
base := "/run/secrets"
|
||||
if secret.Target != "" {
|
||||
secretFileName = secret.Target
|
||||
//If absolute path for target given remove base.
|
||||
if filepath.IsAbs(secretFileName) {
|
||||
base = ""
|
||||
}
|
||||
}
|
||||
src := filepath.Join(c.config.SecretsPath, secret.Name)
|
||||
dest := filepath.Join(base, secretFileName)
|
||||
c.state.BindMounts[dest] = src
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateResolvConf generates a containers resolv.conf
|
||||
func (c *Container) generateResolvConf() error {
|
||||
var (
|
||||
networkNameServers []string
|
||||
networkSearchDomains []string
|
||||
)
|
||||
|
||||
netStatus := c.getNetworkStatus()
|
||||
for _, status := range netStatus {
|
||||
if status.DNSServerIPs != nil {
|
||||
for _, nsIP := range status.DNSServerIPs {
|
||||
networkNameServers = append(networkNameServers, nsIP.String())
|
||||
}
|
||||
logrus.Debugf("Adding nameserver(s) from network status of '%q'", status.DNSServerIPs)
|
||||
}
|
||||
if status.DNSSearchDomains != nil {
|
||||
networkSearchDomains = append(networkSearchDomains, status.DNSSearchDomains...)
|
||||
logrus.Debugf("Adding search domain(s) from network status of '%q'", status.DNSSearchDomains)
|
||||
}
|
||||
}
|
||||
|
||||
ipv6, err := c.checkForIPv6(netStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nameservers := make([]string, 0, len(c.runtime.config.Containers.DNSServers)+len(c.config.DNSServer))
|
||||
nameservers = append(nameservers, c.runtime.config.Containers.DNSServers...)
|
||||
for _, ip := range c.config.DNSServer {
|
||||
nameservers = append(nameservers, ip.String())
|
||||
}
|
||||
// If the user provided dns, it trumps all; then dns masq; then resolv.conf
|
||||
var search []string
|
||||
keepHostServers := false
|
||||
if len(nameservers) == 0 {
|
||||
keepHostServers = true
|
||||
// first add the nameservers from the networks status
|
||||
nameservers = networkNameServers
|
||||
// when we add network dns server we also have to add the search domains
|
||||
search = networkSearchDomains
|
||||
}
|
||||
|
||||
if len(c.config.DNSSearch) > 0 || len(c.runtime.config.Containers.DNSSearches) > 0 {
|
||||
customSearch := make([]string, 0, len(c.config.DNSSearch)+len(c.runtime.config.Containers.DNSSearches))
|
||||
customSearch = append(customSearch, c.runtime.config.Containers.DNSSearches...)
|
||||
customSearch = append(customSearch, c.config.DNSSearch...)
|
||||
search = customSearch
|
||||
}
|
||||
|
||||
options := make([]string, 0, len(c.config.DNSOption)+len(c.runtime.config.Containers.DNSOptions))
|
||||
options = append(options, c.runtime.config.Containers.DNSOptions...)
|
||||
options = append(options, c.config.DNSOption...)
|
||||
|
||||
destPath := filepath.Join(c.state.RunDir, "resolv.conf")
|
||||
|
||||
if err := resolvconf.New(&resolvconf.Params{
|
||||
IPv6Enabled: ipv6,
|
||||
KeepHostServers: keepHostServers,
|
||||
Nameservers: nameservers,
|
||||
Options: options,
|
||||
Path: destPath,
|
||||
Searches: search,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error building resolv.conf for container %s: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return c.bindMountRootFile(destPath, resolvconf.DefaultResolvConf)
|
||||
}
|
||||
|
||||
// Check if a container uses IPv6.
|
||||
func (c *Container) checkForIPv6(netStatus map[string]types.StatusBlock) (bool, error) {
|
||||
for _, status := range netStatus {
|
||||
for _, netInt := range status.Interfaces {
|
||||
for _, netAddress := range netInt.Subnets {
|
||||
// Note: only using To16() does not work since it also returns a valid ip for ipv4
|
||||
if netAddress.IPNet.IP.To4() == nil && netAddress.IPNet.IP.To16() != nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Add a new nameserver to the container's resolv.conf, ensuring that it is the
|
||||
// first nameserver present.
|
||||
// Usable only with running containers.
|
||||
func (c *Container) addNameserver(ips []string) error {
|
||||
// Take no action if container is not running.
|
||||
if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do we have a resolv.conf at all?
|
||||
path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := resolvconf.Add(path, ips); err != nil {
|
||||
return fmt.Errorf("adding new nameserver to container %s resolv.conf: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove an entry from the existing resolv.conf of the container.
|
||||
// Usable only with running containers.
|
||||
func (c *Container) removeNameserver(ips []string) error {
|
||||
// Take no action if container is not running.
|
||||
if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do we have a resolv.conf at all?
|
||||
path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := resolvconf.Remove(path, ips); err != nil {
|
||||
return fmt.Errorf("removing nameservers from container %s resolv.conf: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalhostHostEntry(c *Container) etchosts.HostEntries {
|
||||
return etchosts.HostEntries{{IP: "127.0.0.1", Names: []string{c.Hostname(), c.config.Name}}}
|
||||
}
|
||||
|
||||
// getHostsEntries returns the container ip host entries for the correct netmode
|
||||
func (c *Container) getHostsEntries() (etchosts.HostEntries, error) {
|
||||
var entries etchosts.HostEntries
|
||||
names := []string{c.Hostname(), c.config.Name}
|
||||
switch {
|
||||
case c.config.NetMode.IsBridge():
|
||||
entries = etchosts.GetNetworkHostEntries(c.state.NetworkStatus, names...)
|
||||
default:
|
||||
// check for net=none
|
||||
/*if !c.config.CreateNetNS {
|
||||
for _, ns := range c.config.Spec.Linux.Namespaces {
|
||||
if ns.Type == spec.NetworkNamespace {
|
||||
if ns.Path == "" {
|
||||
entries = etchosts.HostEntries{{IP: "127.0.0.1", Names: names}}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (c *Container) createHosts() error {
|
||||
var containerIPsEntries etchosts.HostEntries
|
||||
var err error
|
||||
// if we configure the netns after the container create we should not add
|
||||
// the hosts here since we have no information about the actual ips
|
||||
// instead we will add them in c.completeNetworkSetup()
|
||||
if !c.config.PostConfigureNetNS {
|
||||
containerIPsEntries, err = c.getHostsEntries()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get container ip host entries: %w", err)
|
||||
}
|
||||
}
|
||||
baseHostFile, err := etchosts.GetBaseHostFile(c.runtime.config.Containers.BaseHostsFile, c.state.Mountpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile := filepath.Join(c.state.RunDir, "hosts")
|
||||
err = etchosts.New(&etchosts.Params{
|
||||
BaseFile: baseHostFile,
|
||||
ExtraHosts: c.config.HostAdd,
|
||||
ContainerIPs: containerIPsEntries,
|
||||
HostContainersInternalIP: etchosts.GetHostContainersInternalIP(c.runtime.config, c.state.NetworkStatus, c.runtime.network),
|
||||
TargetFile: targetFile,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.bindMountRootFile(targetFile, config.DefaultHostsFile)
|
||||
}
|
||||
|
||||
// bindMountRootFile will chown and relabel the source file to make it usable in the container.
|
||||
// It will also add the path to the container bind mount map.
|
||||
// source is the path on the host, dest is the path in the container.
|
||||
func (c *Container) bindMountRootFile(source, dest string) error {
|
||||
if err := os.Chown(source, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := label.Relabel(source, c.MountLabel(), false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.mountIntoRootDirs(dest, source)
|
||||
}
|
||||
|
||||
// generateGroupEntry generates an entry or entries into /etc/group as
|
||||
// required by container configuration.
|
||||
// Generally 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, fmt.Errorf("failed to get current group: %w", err)
|
||||
}
|
||||
|
||||
// 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, fmt.Errorf("failed to get current user to make group entry: %w", err)
|
||||
}
|
||||
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 // nolint: nilerr
|
||||
}
|
||||
|
||||
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).
|
||||
// 3. The user specified additional host user accounts to add the the /etc/passwd file
|
||||
// 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
|
||||
for _, userid := range c.config.HostUsers {
|
||||
// Lookup User on host
|
||||
u, err := util.LookupUser(userid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
entry, err := c.userPasswdEntry(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwdString += entry
|
||||
}
|
||||
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.
|
||||
// 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 "", 0, 0, nil
|
||||
}
|
||||
|
||||
u, err := user.LookupId(strconv.Itoa(uid))
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
pwd, err := c.userPasswdEntry(u)
|
||||
if err != nil {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
|
||||
return pwd, uid, rootless.GetRootlessGID(), nil
|
||||
}
|
||||
|
||||
func (c *Container) userPasswdEntry(u *user.User) (string, error) {
|
||||
// Lookup the user to see if it exists in the container image.
|
||||
_, err := lookup.GetUser(c.state.Mountpoint, u.Username)
|
||||
if err != runcuser.ErrNoPasswdEntries {
|
||||
return "", 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 "", err
|
||||
}
|
||||
|
||||
// If the user's actual home directory exists, or was mounted in - use
|
||||
// that.
|
||||
homeDir := c.WorkingDir()
|
||||
hDir := u.HomeDir
|
||||
for hDir != "/" {
|
||||
if MountExists(c.config.Spec.Mounts, hDir) {
|
||||
homeDir = u.HomeDir
|
||||
break
|
||||
}
|
||||
hDir = filepath.Dir(hDir)
|
||||
}
|
||||
if homeDir != u.HomeDir {
|
||||
for _, hDir := range c.UserVolumes() {
|
||||
if hDir == u.HomeDir {
|
||||
homeDir = u.HomeDir
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set HOME environment if not already set
|
||||
hasHomeSet := false
|
||||
for _, s := range c.config.Spec.Process.Env {
|
||||
if strings.HasPrefix(s, "HOME=") {
|
||||
hasHomeSet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasHomeSet {
|
||||
c.config.Spec.Process.Env = append(c.config.Spec.Process.Env, fmt.Sprintf("HOME=%s", homeDir))
|
||||
}
|
||||
if c.config.PasswdEntry != "" {
|
||||
return c.passwdEntry(u.Username, u.Uid, u.Gid, u.Name, homeDir), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Name, homeDir), nil
|
||||
}
|
||||
|
||||
// generateUserPasswdEntry generates an /etc/passwd entry for the container user
|
||||
// to run in the container.
|
||||
// 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 "", 0, 0, nil
|
||||
}
|
||||
splitSpec := strings.SplitN(c.config.User, ":", 2)
|
||||
userspec := splitSpec[0]
|
||||
if len(splitSpec) > 1 {
|
||||
groupspec = splitSpec[1]
|
||||
}
|
||||
// If a non numeric User, then don't generate passwd
|
||||
uid, err := strconv.ParseUint(userspec, 10, 32)
|
||||
if err != nil {
|
||||
return "", 0, 0, nil // nolint: nilerr
|
||||
}
|
||||
|
||||
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 != runcuser.ErrNoPasswdEntries {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
|
||||
if groupspec != "" {
|
||||
ugid, err := strconv.ParseUint(groupspec, 10, 32)
|
||||
if err == nil {
|
||||
gid = int(ugid)
|
||||
} else {
|
||||
group, err := lookup.GetGroup(c.state.Mountpoint, groupspec)
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("unable to get gid %s from group file: %w", groupspec, err)
|
||||
}
|
||||
gid = group.Gid
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.PasswdEntry != "" {
|
||||
entry := c.passwdEntry(fmt.Sprintf("%d", uid), fmt.Sprintf("%d", uid), fmt.Sprintf("%d", gid), "container user", c.WorkingDir())
|
||||
return entry, int(uid), gid, nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d:*:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), int(uid), gid, nil
|
||||
}
|
||||
|
||||
func (c *Container) passwdEntry(username string, uid, gid, name, homeDir string) string {
|
||||
s := c.config.PasswdEntry
|
||||
s = strings.Replace(s, "$USERNAME", username, -1)
|
||||
s = strings.Replace(s, "$UID", uid, -1)
|
||||
s = strings.Replace(s, "$GID", gid, -1)
|
||||
s = strings.Replace(s, "$NAME", name, -1)
|
||||
s = strings.Replace(s, "$HOME", homeDir, -1)
|
||||
return s + "\n"
|
||||
}
|
||||
|
||||
// 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 or the user specified HostsUsers
|
||||
// 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 == "" &&
|
||||
len(c.config.HostUsers) == 0 {
|
||||
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") {
|
||||
needPasswd = false
|
||||
}
|
||||
if MountExists(c.config.Spec.Mounts, "/etc/group") {
|
||||
needGroup = false
|
||||
}
|
||||
|
||||
// Next, check if we already made the files. If we didn't, 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 needGroup {
|
||||
groupPath := filepath.Join(c.config.StaticDir, "group")
|
||||
if _, err := os.Stat(groupPath); err == nil {
|
||||
needGroup = false
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
needPasswd = exists
|
||||
}
|
||||
if needGroup {
|
||||
exists, err := c.checkFileExistsInRootfs("/etc/group")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
needGroup = exists
|
||||
}
|
||||
|
||||
// If we don't need a /etc/passwd or /etc/group at this point we can
|
||||
// just return.
|
||||
if !needPasswd && !needGroup {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
passwdPath := ""
|
||||
groupPath := ""
|
||||
|
||||
ro := c.IsReadOnly()
|
||||
|
||||
if needPasswd {
|
||||
passwdEntry, err := c.generatePasswdEntry()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
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 "", "", fmt.Errorf("error creating path to container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
orig, err := ioutil.ReadFile(originPasswdFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", "", err
|
||||
}
|
||||
passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+passwdEntry)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create temporary passwd file: %w", err)
|
||||
}
|
||||
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 "", "", fmt.Errorf("error looking up location of container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(containerPasswd, os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(passwdEntry); err != nil {
|
||||
return "", "", fmt.Errorf("unable to append to container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
default:
|
||||
logrus.Debugf("Not modifying container %s /etc/passwd", c.ID())
|
||||
}
|
||||
}
|
||||
if needGroup {
|
||||
groupEntry, err := c.generateGroupEntry()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
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 "", "", fmt.Errorf("error creating path to container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
orig, err := ioutil.ReadFile(originGroupFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", "", err
|
||||
}
|
||||
groupFile, err := c.writeStringToStaticDir("group", string(orig)+groupEntry)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create temporary group file: %w", err)
|
||||
}
|
||||
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 "", "", fmt.Errorf("error looking up location of container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(containerGroup, os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(groupEntry); err != nil {
|
||||
return "", "", fmt.Errorf("unable to append to container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
default:
|
||||
logrus.Debugf("Not modifying container %s /etc/group", c.ID())
|
||||
}
|
||||
}
|
||||
|
||||
return passwdPath, groupPath, nil
|
||||
}
|
||||
|
||||
func isRootlessCgroupSet(cgroup string) bool {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,9 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
|
@ -20,24 +17,15 @@ import (
|
|||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containers/buildah/pkg/overlay"
|
||||
"github.com/containers/common/libnetwork/etchosts"
|
||||
"github.com/containers/common/libnetwork/resolvconf"
|
||||
"github.com/containers/common/libnetwork/types"
|
||||
"github.com/containers/common/pkg/cgroups"
|
||||
"github.com/containers/common/pkg/chown"
|
||||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/common/pkg/subscriptions"
|
||||
"github.com/containers/common/pkg/umask"
|
||||
"github.com/containers/podman/v4/libpod/define"
|
||||
"github.com/containers/podman/v4/pkg/lookup"
|
||||
"github.com/containers/podman/v4/pkg/rootless"
|
||||
"github.com/containers/podman/v4/pkg/util"
|
||||
"github.com/containers/podman/v4/utils"
|
||||
"github.com/containers/podman/v4/version"
|
||||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/lockfile"
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
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"
|
||||
|
|
@ -343,956 +331,6 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr
|
|||
return nil
|
||||
}
|
||||
|
||||
// Ensure standard bind mounts are mounted into all root directories (including chroot directories)
|
||||
func (c *Container) mountIntoRootDirs(mountName string, mountPath string) error {
|
||||
c.state.BindMounts[mountName] = mountPath
|
||||
|
||||
for _, chrootDir := range c.config.ChrootDirs {
|
||||
c.state.BindMounts[filepath.Join(chrootDir, mountName)] = mountPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make standard bind mounts to include in the container
|
||||
func (c *Container) makeBindMounts() error {
|
||||
if err := os.Chown(c.state.RunDir, c.RootUID(), c.RootGID()); err != nil {
|
||||
return fmt.Errorf("cannot chown run directory: %w", err)
|
||||
}
|
||||
|
||||
if c.state.BindMounts == nil {
|
||||
c.state.BindMounts = make(map[string]string)
|
||||
}
|
||||
netDisabled, err := c.NetworkDisabled()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !netDisabled {
|
||||
// If /etc/resolv.conf and /etc/hosts exist, delete them so we
|
||||
// will recreate. Only do this if we aren't sharing them with
|
||||
// another container.
|
||||
if c.config.NetNsCtr == "" {
|
||||
if resolvePath, ok := c.state.BindMounts["/etc/resolv.conf"]; ok {
|
||||
if err := os.Remove(resolvePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
delete(c.state.BindMounts, "/etc/resolv.conf")
|
||||
}
|
||||
if hostsPath, ok := c.state.BindMounts["/etc/hosts"]; ok {
|
||||
if err := os.Remove(hostsPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
delete(c.state.BindMounts, "/etc/hosts")
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.NetNsCtr != "" && (!c.config.UseImageResolvConf || !c.config.UseImageHosts) {
|
||||
// We share a net namespace.
|
||||
// We want /etc/resolv.conf and /etc/hosts from the
|
||||
// other container. Unless we're not creating both of
|
||||
// them.
|
||||
depCtr, err := c.getRootNetNsDepCtr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching network namespace dependency container for container %s: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
// We need that container's bind mounts
|
||||
bindMounts, err := depCtr.BindMounts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching bind mounts from dependency %s of container %s: %w", depCtr.ID(), c.ID(), err)
|
||||
}
|
||||
|
||||
// The other container may not have a resolv.conf or /etc/hosts
|
||||
// If it doesn't, don't copy them
|
||||
resolvPath, exists := bindMounts["/etc/resolv.conf"]
|
||||
if !c.config.UseImageResolvConf && exists {
|
||||
err := c.mountIntoRootDirs("/etc/resolv.conf", resolvPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// check if dependency container has an /etc/hosts file.
|
||||
// It may not have one, so only use it if it does.
|
||||
hostsPath, exists := bindMounts[config.DefaultHostsFile]
|
||||
if !c.config.UseImageHosts && exists {
|
||||
// we cannot use the dependency container lock due ABBA deadlocks in cleanup()
|
||||
lock, err := lockfile.GetLockfile(hostsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lock hosts file: %w", err)
|
||||
}
|
||||
lock.Lock()
|
||||
|
||||
// add the newly added container to the hosts file
|
||||
// we always use 127.0.0.1 as ip since they have the same netns
|
||||
err = etchosts.Add(hostsPath, getLocalhostHostEntry(c))
|
||||
lock.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s which depends on container %s: %w", c.ID(), depCtr.ID(), err)
|
||||
}
|
||||
|
||||
// finally, save it in the new container
|
||||
err = c.mountIntoRootDirs(config.DefaultHostsFile, hostsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasCurrentUserMapped(c) {
|
||||
if err := makeAccessible(resolvPath, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := makeAccessible(hostsPath, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !c.config.UseImageResolvConf {
|
||||
if err := c.generateResolvConf(); err != nil {
|
||||
return fmt.Errorf("error creating resolv.conf for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if !c.config.UseImageHosts {
|
||||
if err := c.createHosts(); err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.state.BindMounts["/etc/hosts"] != "" {
|
||||
if err := c.relabel(c.state.BindMounts["/etc/hosts"], c.config.MountLabel, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.state.BindMounts["/etc/resolv.conf"] != "" {
|
||||
if err := c.relabel(c.state.BindMounts["/etc/resolv.conf"], c.config.MountLabel, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !c.config.UseImageHosts && c.state.BindMounts["/etc/hosts"] == "" {
|
||||
if err := c.createHosts(); err != nil {
|
||||
return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.ShmDir != "" {
|
||||
// If ShmDir has a value SHM is always added when we mount the container
|
||||
c.state.BindMounts["/dev/shm"] = c.config.ShmDir
|
||||
}
|
||||
|
||||
if c.config.Passwd == nil || *c.config.Passwd {
|
||||
newPasswd, newGroup, err := c.generatePasswdAndGroup()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating temporary passwd file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
if newPasswd != "" {
|
||||
// Make /etc/passwd
|
||||
// If it already exists, delete so we can recreate
|
||||
delete(c.state.BindMounts, "/etc/passwd")
|
||||
c.state.BindMounts["/etc/passwd"] = newPasswd
|
||||
}
|
||||
if newGroup != "" {
|
||||
// Make /etc/group
|
||||
// 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
|
||||
if _, ok := c.state.BindMounts["/etc/hostname"]; !ok {
|
||||
hostnamePath, err := c.writeStringToRundir("hostname", c.Hostname())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating hostname file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
c.state.BindMounts["/etc/hostname"] = hostnamePath
|
||||
}
|
||||
|
||||
// Make /etc/localtime
|
||||
ctrTimezone := c.Timezone()
|
||||
if ctrTimezone != "" {
|
||||
// validate the format of the timezone specified if it's not "local"
|
||||
if ctrTimezone != "local" {
|
||||
_, err = time.LoadLocation(ctrTimezone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
if _, ok := c.state.BindMounts["/etc/localtime"]; !ok {
|
||||
var zonePath string
|
||||
if ctrTimezone == "local" {
|
||||
zonePath, err = filepath.EvalSymlinks("/etc/localtime")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding local timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
} else {
|
||||
zone := filepath.Join("/usr/share/zoneinfo", ctrTimezone)
|
||||
zonePath, err = filepath.EvalSymlinks(zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
}
|
||||
localtimePath, err := c.copyTimezoneFile(zonePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err)
|
||||
}
|
||||
c.state.BindMounts["/etc/localtime"] = localtimePath
|
||||
}
|
||||
}
|
||||
|
||||
_, hasRunContainerenv := c.state.BindMounts["/run/.containerenv"]
|
||||
if !hasRunContainerenv {
|
||||
// check in the spec mounts
|
||||
for _, m := range c.config.Spec.Mounts {
|
||||
if m.Destination == "/run/.containerenv" || m.Destination == "/run" {
|
||||
hasRunContainerenv = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make .containerenv if it does not exist
|
||||
if !hasRunContainerenv {
|
||||
containerenv := c.runtime.graphRootMountedFlag(c.config.Spec.Mounts)
|
||||
isRootless := 0
|
||||
if rootless.IsRootless() {
|
||||
isRootless = 1
|
||||
}
|
||||
imageID, imageName := c.Image()
|
||||
|
||||
if c.Privileged() {
|
||||
// Populate the .containerenv with container information
|
||||
containerenv = fmt.Sprintf(`engine="podman-%s"
|
||||
name=%q
|
||||
id=%q
|
||||
image=%q
|
||||
imageid=%q
|
||||
rootless=%d
|
||||
%s`, version.Version.String(), c.Name(), c.ID(), imageName, imageID, isRootless, containerenv)
|
||||
}
|
||||
containerenvPath, err := c.writeStringToRundir(".containerenv", containerenv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating containerenv file for container %s: %w", c.ID(), err)
|
||||
}
|
||||
c.state.BindMounts["/run/.containerenv"] = containerenvPath
|
||||
}
|
||||
|
||||
// Add Subscription Mounts
|
||||
subscriptionMounts := subscriptions.MountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.Containers.DefaultMountsFile, c.state.Mountpoint, c.RootUID(), c.RootGID(), rootless.IsRootless(), false)
|
||||
for _, mount := range subscriptionMounts {
|
||||
if _, ok := c.state.BindMounts[mount.Destination]; !ok {
|
||||
c.state.BindMounts[mount.Destination] = mount.Source
|
||||
}
|
||||
}
|
||||
|
||||
// Secrets are mounted by getting the secret data from the secrets manager,
|
||||
// copying the data into the container's static dir,
|
||||
// then mounting the copied dir into /run/secrets.
|
||||
// The secrets mounting must come after subscription mounts, since subscription mounts
|
||||
// creates the /run/secrets dir in the container where we mount as well.
|
||||
if len(c.Secrets()) > 0 {
|
||||
// create /run/secrets if subscriptions did not create
|
||||
if err := c.createSecretMountDir(); err != nil {
|
||||
return fmt.Errorf("error creating secrets mount: %w", err)
|
||||
}
|
||||
for _, secret := range c.Secrets() {
|
||||
secretFileName := secret.Name
|
||||
base := "/run/secrets"
|
||||
if secret.Target != "" {
|
||||
secretFileName = secret.Target
|
||||
// If absolute path for target given remove base.
|
||||
if filepath.IsAbs(secretFileName) {
|
||||
base = ""
|
||||
}
|
||||
}
|
||||
src := filepath.Join(c.config.SecretsPath, secret.Name)
|
||||
dest := filepath.Join(base, secretFileName)
|
||||
c.state.BindMounts[dest] = src
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateResolvConf generates a containers resolv.conf
|
||||
func (c *Container) generateResolvConf() error {
|
||||
var (
|
||||
networkNameServers []string
|
||||
networkSearchDomains []string
|
||||
)
|
||||
|
||||
netStatus := c.getNetworkStatus()
|
||||
for _, status := range netStatus {
|
||||
if status.DNSServerIPs != nil {
|
||||
for _, nsIP := range status.DNSServerIPs {
|
||||
networkNameServers = append(networkNameServers, nsIP.String())
|
||||
}
|
||||
logrus.Debugf("Adding nameserver(s) from network status of '%q'", status.DNSServerIPs)
|
||||
}
|
||||
if status.DNSSearchDomains != nil {
|
||||
networkSearchDomains = append(networkSearchDomains, status.DNSSearchDomains...)
|
||||
logrus.Debugf("Adding search domain(s) from network status of '%q'", status.DNSSearchDomains)
|
||||
}
|
||||
}
|
||||
|
||||
ipv6, err := c.checkForIPv6(netStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nameservers := make([]string, 0, len(c.runtime.config.Containers.DNSServers)+len(c.config.DNSServer))
|
||||
nameservers = append(nameservers, c.runtime.config.Containers.DNSServers...)
|
||||
for _, ip := range c.config.DNSServer {
|
||||
nameservers = append(nameservers, ip.String())
|
||||
}
|
||||
// If the user provided dns, it trumps all; then dns masq; then resolv.conf
|
||||
var search []string
|
||||
keepHostServers := false
|
||||
if len(nameservers) == 0 {
|
||||
keepHostServers = true
|
||||
// first add the nameservers from the networks status
|
||||
nameservers = networkNameServers
|
||||
// when we add network dns server we also have to add the search domains
|
||||
search = networkSearchDomains
|
||||
// slirp4netns has a built in DNS forwarder.
|
||||
if c.config.NetMode.IsSlirp4netns() {
|
||||
slirp4netnsDNS, err := GetSlirp4netnsDNS(c.slirp4netnsSubnet)
|
||||
if err != nil {
|
||||
logrus.Warn("Failed to determine Slirp4netns DNS: ", err.Error())
|
||||
} else {
|
||||
nameservers = append(nameservers, slirp4netnsDNS.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.config.DNSSearch) > 0 || len(c.runtime.config.Containers.DNSSearches) > 0 {
|
||||
customSearch := make([]string, 0, len(c.config.DNSSearch)+len(c.runtime.config.Containers.DNSSearches))
|
||||
customSearch = append(customSearch, c.runtime.config.Containers.DNSSearches...)
|
||||
customSearch = append(customSearch, c.config.DNSSearch...)
|
||||
search = customSearch
|
||||
}
|
||||
|
||||
options := make([]string, 0, len(c.config.DNSOption)+len(c.runtime.config.Containers.DNSOptions))
|
||||
options = append(options, c.runtime.config.Containers.DNSOptions...)
|
||||
options = append(options, c.config.DNSOption...)
|
||||
|
||||
destPath := filepath.Join(c.state.RunDir, "resolv.conf")
|
||||
|
||||
if err := resolvconf.New(&resolvconf.Params{
|
||||
IPv6Enabled: ipv6,
|
||||
KeepHostServers: keepHostServers,
|
||||
Nameservers: nameservers,
|
||||
Namespaces: c.config.Spec.Linux.Namespaces,
|
||||
Options: options,
|
||||
Path: destPath,
|
||||
Searches: search,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error building resolv.conf for container %s: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return c.bindMountRootFile(destPath, resolvconf.DefaultResolvConf)
|
||||
}
|
||||
|
||||
// Check if a container uses IPv6.
|
||||
func (c *Container) checkForIPv6(netStatus map[string]types.StatusBlock) (bool, error) {
|
||||
for _, status := range netStatus {
|
||||
for _, netInt := range status.Interfaces {
|
||||
for _, netAddress := range netInt.Subnets {
|
||||
// Note: only using To16() does not work since it also returns a valid ip for ipv4
|
||||
if netAddress.IPNet.IP.To4() == nil && netAddress.IPNet.IP.To16() != nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.NetMode.IsSlirp4netns() {
|
||||
ctrNetworkSlipOpts := []string{}
|
||||
if c.config.NetworkOptions != nil {
|
||||
ctrNetworkSlipOpts = append(ctrNetworkSlipOpts, c.config.NetworkOptions["slirp4netns"]...)
|
||||
}
|
||||
slirpOpts, err := parseSlirp4netnsNetworkOptions(c.runtime, ctrNetworkSlipOpts)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return slirpOpts.enableIPv6, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Add a new nameserver to the container's resolv.conf, ensuring that it is the
|
||||
// first nameserver present.
|
||||
// Usable only with running containers.
|
||||
func (c *Container) addNameserver(ips []string) error {
|
||||
// Take no action if container is not running.
|
||||
if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do we have a resolv.conf at all?
|
||||
path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := resolvconf.Add(path, ips); err != nil {
|
||||
return fmt.Errorf("adding new nameserver to container %s resolv.conf: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove an entry from the existing resolv.conf of the container.
|
||||
// Usable only with running containers.
|
||||
func (c *Container) removeNameserver(ips []string) error {
|
||||
// Take no action if container is not running.
|
||||
if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do we have a resolv.conf at all?
|
||||
path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := resolvconf.Remove(path, ips); err != nil {
|
||||
return fmt.Errorf("removing nameservers from container %s resolv.conf: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalhostHostEntry(c *Container) etchosts.HostEntries {
|
||||
return etchosts.HostEntries{{IP: "127.0.0.1", Names: []string{c.Hostname(), c.config.Name}}}
|
||||
}
|
||||
|
||||
// getHostsEntries returns the container ip host entries for the correct netmode
|
||||
func (c *Container) getHostsEntries() (etchosts.HostEntries, error) {
|
||||
var entries etchosts.HostEntries
|
||||
names := []string{c.Hostname(), c.config.Name}
|
||||
switch {
|
||||
case c.config.NetMode.IsBridge():
|
||||
entries = etchosts.GetNetworkHostEntries(c.state.NetworkStatus, names...)
|
||||
case c.config.NetMode.IsSlirp4netns():
|
||||
ip, err := GetSlirp4netnsIP(c.slirp4netnsSubnet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = etchosts.HostEntries{{IP: ip.String(), Names: names}}
|
||||
default:
|
||||
// check for net=none
|
||||
if !c.config.CreateNetNS {
|
||||
for _, ns := range c.config.Spec.Linux.Namespaces {
|
||||
if ns.Type == spec.NetworkNamespace {
|
||||
if ns.Path == "" {
|
||||
entries = etchosts.HostEntries{{IP: "127.0.0.1", Names: names}}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (c *Container) createHosts() error {
|
||||
var containerIPsEntries etchosts.HostEntries
|
||||
var err error
|
||||
// if we configure the netns after the container create we should not add
|
||||
// the hosts here since we have no information about the actual ips
|
||||
// instead we will add them in c.completeNetworkSetup()
|
||||
if !c.config.PostConfigureNetNS {
|
||||
containerIPsEntries, err = c.getHostsEntries()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get container ip host entries: %w", err)
|
||||
}
|
||||
}
|
||||
baseHostFile, err := etchosts.GetBaseHostFile(c.runtime.config.Containers.BaseHostsFile, c.state.Mountpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile := filepath.Join(c.state.RunDir, "hosts")
|
||||
err = etchosts.New(&etchosts.Params{
|
||||
BaseFile: baseHostFile,
|
||||
ExtraHosts: c.config.HostAdd,
|
||||
ContainerIPs: containerIPsEntries,
|
||||
HostContainersInternalIP: etchosts.GetHostContainersInternalIP(c.runtime.config, c.state.NetworkStatus, c.runtime.network),
|
||||
TargetFile: targetFile,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.bindMountRootFile(targetFile, config.DefaultHostsFile)
|
||||
}
|
||||
|
||||
// bindMountRootFile will chown and relabel the source file to make it usable in the container.
|
||||
// It will also add the path to the container bind mount map.
|
||||
// source is the path on the host, dest is the path in the container.
|
||||
func (c *Container) bindMountRootFile(source, dest string) error {
|
||||
if err := os.Chown(source, c.RootUID(), c.RootGID()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := label.Relabel(source, c.MountLabel(), false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.mountIntoRootDirs(dest, source)
|
||||
}
|
||||
|
||||
// generateGroupEntry generates an entry or entries into /etc/group as
|
||||
// required by container configuration.
|
||||
// Generally 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, fmt.Errorf("failed to get current group: %w", err)
|
||||
}
|
||||
|
||||
// Look up 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
|
||||
}
|
||||
|
||||
// Look up 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, fmt.Errorf("failed to get current user to make group entry: %w", err)
|
||||
}
|
||||
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, error) {
|
||||
if c.config.User == "" {
|
||||
return "", 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 "", nil //nolint: nilerr
|
||||
}
|
||||
|
||||
if addedGID != 0 && addedGID == int(gid) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Check if the group already exists
|
||||
_, err = lookup.GetGroup(c.state.Mountpoint, group)
|
||||
if err != runcuser.ErrNoGroupEntries {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d:x:%d:%s\n", gid, gid, splitUser[0]), 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).
|
||||
// 3. The user specified additional host user accounts to add the the /etc/passwd file
|
||||
//
|
||||
// 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
|
||||
for _, userid := range c.config.HostUsers {
|
||||
// Look up User on host
|
||||
u, err := util.LookupUser(userid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
entry, err := c.userPasswdEntry(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwdString += entry
|
||||
}
|
||||
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.
|
||||
// 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 "", 0, 0, nil
|
||||
}
|
||||
|
||||
u, err := user.LookupId(strconv.Itoa(uid))
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
pwd, err := c.userPasswdEntry(u)
|
||||
if err != nil {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
|
||||
return pwd, uid, rootless.GetRootlessGID(), nil
|
||||
}
|
||||
|
||||
func (c *Container) userPasswdEntry(u *user.User) (string, error) {
|
||||
// Look up the user to see if it exists in the container image.
|
||||
_, err := lookup.GetUser(c.state.Mountpoint, u.Username)
|
||||
if err != runcuser.ErrNoPasswdEntries {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Look up the UID to see if it exists in the container image.
|
||||
_, err = lookup.GetUser(c.state.Mountpoint, u.Uid)
|
||||
if err != runcuser.ErrNoPasswdEntries {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If the user's actual home directory exists, or was mounted in - use
|
||||
// that.
|
||||
homeDir := c.WorkingDir()
|
||||
hDir := u.HomeDir
|
||||
for hDir != "/" {
|
||||
if MountExists(c.config.Spec.Mounts, hDir) {
|
||||
homeDir = u.HomeDir
|
||||
break
|
||||
}
|
||||
hDir = filepath.Dir(hDir)
|
||||
}
|
||||
if homeDir != u.HomeDir {
|
||||
for _, hDir := range c.UserVolumes() {
|
||||
if hDir == u.HomeDir {
|
||||
homeDir = u.HomeDir
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set HOME environment if not already set
|
||||
hasHomeSet := false
|
||||
for _, s := range c.config.Spec.Process.Env {
|
||||
if strings.HasPrefix(s, "HOME=") {
|
||||
hasHomeSet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasHomeSet {
|
||||
c.config.Spec.Process.Env = append(c.config.Spec.Process.Env, fmt.Sprintf("HOME=%s", homeDir))
|
||||
}
|
||||
if c.config.PasswdEntry != "" {
|
||||
return c.passwdEntry(u.Username, u.Uid, u.Gid, u.Name, homeDir), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Name, homeDir), nil
|
||||
}
|
||||
|
||||
// generateUserPasswdEntry generates an /etc/passwd entry for the container user
|
||||
// to run in the container.
|
||||
// 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, error) {
|
||||
var (
|
||||
groupspec string
|
||||
gid int
|
||||
)
|
||||
if c.config.User == "" {
|
||||
return "", nil
|
||||
}
|
||||
splitSpec := strings.SplitN(c.config.User, ":", 2)
|
||||
userspec := splitSpec[0]
|
||||
if len(splitSpec) > 1 {
|
||||
groupspec = splitSpec[1]
|
||||
}
|
||||
// If a non numeric User, then don't generate passwd
|
||||
uid, err := strconv.ParseUint(userspec, 10, 32)
|
||||
if err != nil {
|
||||
return "", nil //nolint: nilerr
|
||||
}
|
||||
|
||||
if addedUID != 0 && int(uid) == addedUID {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Look up the user to see if it exists in the container image
|
||||
_, err = lookup.GetUser(c.state.Mountpoint, userspec)
|
||||
if err != runcuser.ErrNoPasswdEntries {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if groupspec != "" {
|
||||
ugid, err := strconv.ParseUint(groupspec, 10, 32)
|
||||
if err == nil {
|
||||
gid = int(ugid)
|
||||
} else {
|
||||
group, err := lookup.GetGroup(c.state.Mountpoint, groupspec)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get gid %s from group file: %w", groupspec, err)
|
||||
}
|
||||
gid = group.Gid
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.PasswdEntry != "" {
|
||||
entry := c.passwdEntry(fmt.Sprintf("%d", uid), fmt.Sprintf("%d", uid), fmt.Sprintf("%d", gid), "container user", c.WorkingDir())
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d:*:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), nil
|
||||
}
|
||||
|
||||
func (c *Container) passwdEntry(username string, uid, gid, name, homeDir string) string {
|
||||
s := c.config.PasswdEntry
|
||||
s = strings.ReplaceAll(s, "$USERNAME", username)
|
||||
s = strings.ReplaceAll(s, "$UID", uid)
|
||||
s = strings.ReplaceAll(s, "$GID", gid)
|
||||
s = strings.ReplaceAll(s, "$NAME", name)
|
||||
s = strings.ReplaceAll(s, "$HOME", homeDir)
|
||||
return s + "\n"
|
||||
}
|
||||
|
||||
// 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 or the user specified HostsUsers
|
||||
// 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 == "" &&
|
||||
len(c.config.HostUsers) == 0 {
|
||||
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") {
|
||||
needPasswd = false
|
||||
}
|
||||
if MountExists(c.config.Spec.Mounts, "/etc/group") {
|
||||
needGroup = false
|
||||
}
|
||||
|
||||
// Next, check if we already made the files. If we didn't, 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 needGroup {
|
||||
groupPath := filepath.Join(c.config.StaticDir, "group")
|
||||
if _, err := os.Stat(groupPath); err == nil {
|
||||
needGroup = false
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't need a /etc/passwd or /etc/group at this point we can
|
||||
// just return.
|
||||
if !needPasswd && !needGroup {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
passwdPath := ""
|
||||
groupPath := ""
|
||||
|
||||
ro := c.IsReadOnly()
|
||||
|
||||
if needPasswd {
|
||||
passwdEntry, err := c.generatePasswdEntry()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
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 "", "", fmt.Errorf("error creating path to container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
orig, err := ioutil.ReadFile(originPasswdFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", "", err
|
||||
}
|
||||
passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+passwdEntry)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create temporary passwd file: %w", err)
|
||||
}
|
||||
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 "", "", fmt.Errorf("error looking up location of container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(containerPasswd, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(passwdEntry); err != nil {
|
||||
return "", "", fmt.Errorf("unable to append to container %s /etc/passwd: %w", c.ID(), err)
|
||||
}
|
||||
default:
|
||||
logrus.Debugf("Not modifying container %s /etc/passwd", c.ID())
|
||||
}
|
||||
}
|
||||
if needGroup {
|
||||
groupEntry, err := c.generateGroupEntry()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
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 "", "", fmt.Errorf("error creating path to container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
orig, err := ioutil.ReadFile(originGroupFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", "", err
|
||||
}
|
||||
groupFile, err := c.writeStringToStaticDir("group", string(orig)+groupEntry)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create temporary group file: %w", err)
|
||||
}
|
||||
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 "", "", fmt.Errorf("error looking up location of container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(containerGroup, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("container %s: %w", c.ID(), err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(groupEntry); err != nil {
|
||||
return "", "", fmt.Errorf("unable to append to container %s /etc/group: %w", c.ID(), err)
|
||||
}
|
||||
default:
|
||||
logrus.Debugf("Not modifying container %s /etc/group", c.ID())
|
||||
}
|
||||
}
|
||||
|
||||
return passwdPath, groupPath, nil
|
||||
}
|
||||
|
||||
func isRootlessCgroupSet(cgroup string) bool {
|
||||
// old versions of podman were setting the CgroupParent to CgroupfsDefaultCgroupParent
|
||||
// by default. Avoid breaking these versions and check whether the cgroup parent is
|
||||
|
|
|
|||
Loading…
Reference in New Issue