From 00d38cb37958f3c636aa5837b8f01dfad891a0b5 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Mon, 18 Dec 2017 12:05:06 -0500 Subject: podman create/run need to load information from the image We should be pulling information out of the image to set the defaults to use when setting up the container. Signed-off-by: Daniel J Walsh Closes: #110 Approved by: mheon --- cmd/podman/create.go | 251 ++++++++++++++++++++++++++++++------------- cmd/podman/inspect.go | 3 +- cmd/podman/parse.go | 73 ------------- cmd/podman/run.go | 59 +++------- cmd/podman/spec.go | 3 - libpod/container.go | 23 +++- libpod/options.go | 13 +++ libpod/runtime_ctr.go | 4 +- libpod/storage.go | 4 +- pkg/chrootuser/user.go | 71 ++++++++++++ pkg/chrootuser/user_basic.go | 19 ++++ pkg/chrootuser/user_linux.go | 235 ++++++++++++++++++++++++++++++++++++++++ 12 files changed, 553 insertions(+), 205 deletions(-) create mode 100644 pkg/chrootuser/user.go create mode 100644 pkg/chrootuser/user_basic.go create mode 100644 pkg/chrootuser/user_linux.go diff --git a/cmd/podman/create.go b/cmd/podman/create.go index 8b64a1cb0..182eb1e56 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/signal" + "github.com/docker/go-connections/nat" "github.com/docker/go-units" "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" @@ -80,10 +81,11 @@ type createConfig struct { DNSServers []string //dns Entrypoint string //entrypoint Env map[string]string //env - Expose []string //expose - GroupAdd []uint32 // group-add - Hostname string //hostname + ExposedPorts map[nat.Port]struct{} + GroupAdd []uint32 // group-add + Hostname string //hostname Image string + ImageID string Interactive bool //interactive IpcMode container.IpcMode //ipc IP6Address string //ipv6 @@ -99,7 +101,8 @@ type createConfig struct { NetworkAlias []string //network-alias PidMode container.PidMode //pid NsUser string - Pod string //pod + Pod string //pod + PortBindings nat.PortMap Privileged bool //privileged Publish []string //publish PublishAll bool //publish-all @@ -115,8 +118,7 @@ type createConfig struct { Sysctl map[string]string //sysctl Tmpfs []string // tmpfs Tty bool //tty - User uint32 //user - Group uint32 // group + User string //user UtsMode container.UTSMode //uts Volumes []string //volume WorkDir string //workdir @@ -148,7 +150,6 @@ var createCommand = cli.Command{ func createCmd(c *cli.Context) error { // TODO should allow user to create based off a directory on the host not just image // Need CLI support for this - var imageName string if err := validateFlags(c, createFlags); err != nil { return err } @@ -164,54 +165,19 @@ func createCmd(c *cli.Context) error { return err } - // Deal with the image after all the args have been checked - createImage := runtime.NewImage(createConfig.Image) - createImage.LocalName, _ = createImage.GetLocalImageName() - if createImage.LocalName == "" { - // The image wasnt found by the user input'd name or its fqname - // Pull the image - var writer io.Writer - if !createConfig.Quiet { - writer = os.Stdout - } - createImage.Pull(writer) - } - runtimeSpec, err := createConfigToOCISpec(createConfig) if err != nil { return err } - if createImage.LocalName != "" { - nameIsID, err := runtime.IsImageID(createImage.LocalName) - if err != nil { - return err - } - if nameIsID { - // If the input from the user is an ID, then we need to get the image - // name for cstorage - createImage.LocalName, err = createImage.GetNameByID() - if err != nil { - return err - } - } - imageName = createImage.LocalName - } else { - imageName, err = createImage.GetFQName() - } - if err != nil { - return err - } - imageID, err := createImage.GetImageID() - if err != nil { - return err - } options, err := createConfig.GetContainerCreateOptions() if err != nil { return errors.Wrapf(err, "unable to parse new container options") } // Gather up the options for NewContainer which consist of With... funcs - options = append(options, libpod.WithRootFSFromImage(imageID, imageName, false)) + options = append(options, libpod.WithRootFSFromImage(createConfig.ImageID, createConfig.Image, true)) options = append(options, libpod.WithSELinuxLabels(createConfig.ProcessLabel, createConfig.MountLabel)) + options = append(options, libpod.WithLabels(createConfig.Labels)) + options = append(options, libpod.WithUser(createConfig.User)) options = append(options, libpod.WithShmDir(createConfig.ShmDir)) ctr, err := runtime.NewContainer(runtimeSpec, options...) if err != nil { @@ -300,13 +266,101 @@ func parseSecurityOpt(config *createConfig, securityOpts []string) error { return err } +func exposedPorts(c *cli.Context, imageExposedPorts map[string]struct{}) (map[nat.Port]struct{}, map[nat.Port][]nat.PortBinding, error) { + // TODO Handle exposed ports from image + // Currently ignoring imageExposedPorts + + ports, portBindings, err := nat.ParsePortSpecs(c.StringSlice("publish")) + if err != nil { + return nil, nil, err + } + + for _, e := range c.StringSlice("expose") { + // Merge in exposed ports to the map of published ports + if strings.Contains(e, ":") { + return nil, nil, fmt.Errorf("invalid port format for --expose: %s", e) + } + //support two formats for expose, original format /[] or /[] + proto, port := nat.SplitProtoPort(e) + //parse the start and end port and create a sequence of ports to expose + //if expose a port, the start and end port are the same + start, end, err := nat.ParsePortRange(port) + if err != nil { + return nil, nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) + } + for i := start; i <= end; i++ { + p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) + if err != nil { + return nil, nil, err + } + if _, exists := ports[p]; !exists { + ports[p] = struct{}{} + } + } + } + return ports, portBindings, nil +} + +// imageData pulls down the image if not stored locally and extracts the +// default container runtime data out of it. imageData returns the data +// to the caller. Example Data: Entrypoint, Env, WorkingDir, Labels ... +func imageData(c *cli.Context, runtime *libpod.Runtime, image string) (string, string, *libpod.ImageData, error) { + var err error + // Deal with the image after all the args have been checked + createImage := runtime.NewImage(image) + createImage.LocalName, _ = createImage.GetLocalImageName() + if createImage.LocalName == "" { + // The image wasnt found by the user input'd name or its fqname + // Pull the image + var writer io.Writer + if !c.Bool("quiet") { + writer = os.Stdout + } + createImage.Pull(writer) + } + + var imageName string + if createImage.LocalName != "" { + nameIsID, err := runtime.IsImageID(createImage.LocalName) + if err != nil { + return "", "", nil, err + } + if nameIsID { + // If the input from the user is an ID, then we need to get the image + // name for cstorage + createImage.LocalName, err = createImage.GetNameByID() + if err != nil { + return "", "", nil, err + } + } + imageName = createImage.LocalName + } else { + imageName, err = createImage.GetFQName() + } + if err != nil { + return "", "", nil, err + } + imageID, err := createImage.GetImageID() + if err != nil { + return "", "", nil, err + } + storageImage, err := runtime.GetImage(image) + if err != nil { + return "", "", nil, errors.Wrapf(err, "error getting storage image %q", image) + } + data, err := runtime.GetImageInspectInfo(*storageImage) + if err != nil { + return "", "", nil, errors.Wrapf(err, "error parsing image data %q", image) + } + return imageName, imageID, data, err +} + // Parses CLI options related to container creation into a config which can be // parsed into an OCI runtime spec func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, error) { var command []string var memoryLimit, memoryReservation, memorySwap, memoryKernel int64 var blkioWeight uint16 - var uid, gid uint32 if len(c.Args()) < 1 { return nil, errors.Errorf("image name or ID is required") @@ -317,33 +371,14 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er command = c.Args()[1:] } - // LABEL VARIABLES - labels, err := getAllLabels(c.StringSlice("label-file"), c.StringSlice("labels")) - if err != nil { - return &createConfig{}, errors.Wrapf(err, "unable to process labels") - } - // ENVIRONMENT VARIABLES - env := defaultEnvVariables - if err := readKVStrings(env, c.StringSlice("env-file"), c.StringSlice("env")); err != nil { - return &createConfig{}, errors.Wrapf(err, "unable to process environment variables") - } - sysctl, err := convertStringSliceToMap(c.StringSlice("sysctl"), "=") if err != nil { - return &createConfig{}, errors.Wrapf(err, "sysctl values must be in the form of KEY=VALUE") + return nil, errors.Wrapf(err, "sysctl values must be in the form of KEY=VALUE") } groupAdd, err := stringSlicetoUint32Slice(c.StringSlice("group-add")) if err != nil { - return &createConfig{}, errors.Wrapf(err, "invalid value for groups provided") - } - - if c.String("user") != "" { - // TODO - // We need to mount the imagefs and get the uid/gid - // For now, user zeros - uid = 0 - gid = 0 + return nil, errors.Wrapf(err, "invalid value for groups provided") } if c.String("memory") != "" { @@ -417,14 +452,79 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er } shmDir = ctr.ShmDir() } - stopSignal := syscall.SIGTERM + + imageName, imageID, data, err := imageData(c, runtime, image) + if err != nil { + return nil, err + } + + // USER + user := c.String("user") + if user == "" { + user = data.Config.User + } + + // STOP SIGNAL + stopSignal := syscall.SIGINT + signalString := data.Config.StopSignal if c.IsSet("stop-signal") { - stopSignal, err = signal.ParseSignal(c.String("stop-signal")) + signalString = c.String("stop-signal") + } + if signalString != "" { + stopSignal, err = signal.ParseSignal(signalString) if err != nil { return nil, err } } + // ENVIRONMENT VARIABLES + env := defaultEnvVariables + for _, e := range data.Config.Env { + split := strings.SplitN(e, "=", 2) + if len(split) > 1 { + env[split[0]] = split[1] + } else { + env[split[0]] = "" + } + } + if err := readKVStrings(env, c.StringSlice("env-file"), c.StringSlice("env")); err != nil { + return nil, errors.Wrapf(err, "unable to process environment variables") + } + + // LABEL VARIABLES + labels, err := getAllLabels(c.StringSlice("label-file"), c.StringSlice("labels")) + if err != nil { + return nil, errors.Wrapf(err, "unable to process labels") + } + for key, val := range data.Config.Labels { + if _, ok := labels[key]; !ok { + labels[key] = val + } + } + + // WORKING DIRECTORY + workDir := c.String("workdir") + if workDir == "" { + workDir = data.Config.WorkingDir + } + + // COMMAND + if len(command) == 0 { + command = data.Config.Cmd + } + + // ENTRYPOINT + entrypoint := c.String("entrypoint") + if entrypoint == "" { + entrypoint = strings.Join(data.Config.Entrypoint, " ") + } + + // EXPOSED PORTS + ports, portBindings, err := exposedPorts(c, data.Config.ExposedPorts) + if err != nil { + return nil, err + } + config := &createConfig{ Runtime: runtime, CapAdd: c.StringSlice("cap-add"), @@ -436,12 +536,13 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er DNSOpt: c.StringSlice("dns-opt"), DNSSearch: c.StringSlice("dns-search"), DNSServers: c.StringSlice("dns"), - Entrypoint: c.String("entrypoint"), + Entrypoint: entrypoint, Env: env, - Expose: c.StringSlice("expose"), + ExposedPorts: ports, GroupAdd: groupAdd, Hostname: c.String("hostname"), - Image: image, + Image: imageName, + ImageID: imageID, Interactive: c.Bool("interactive"), IP6Address: c.String("ipv6"), IPAddress: c.String("ip"), @@ -461,6 +562,7 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er Privileged: c.Bool("privileged"), Publish: c.StringSlice("publish"), PublishAll: c.Bool("publish-all"), + PortBindings: portBindings, Quiet: c.Bool("quiet"), ReadOnlyRootfs: c.Bool("read-only"), Resources: createResourceConfig{ @@ -499,10 +601,9 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er Sysctl: sysctl, Tmpfs: c.StringSlice("tmpfs"), Tty: tty, - User: uid, - Group: gid, + User: user, Volumes: c.StringSlice("volume"), - WorkDir: c.String("workdir"), + WorkDir: workDir, } if !config.Privileged { diff --git a/cmd/podman/inspect.go b/cmd/podman/inspect.go index 7fd039b75..869d16911 100644 --- a/cmd/podman/inspect.go +++ b/cmd/podman/inspect.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" + "github.com/docker/go-connections/nat" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/projectatomic/libpod/cmd/podman/formats" @@ -287,7 +288,7 @@ type HostConfig struct { ContainerIDFile string `json:"ContainerIDFile"` LogConfig *LogConfig `json:"LogConfig"` //TODO NetworkMode string `json:"NetworkMode"` - PortBindings map[string]struct{} `json:"PortBindings"` //TODO + PortBindings nat.PortMap `json:"PortBindings"` //TODO AutoRemove bool `json:"AutoRemove"` CapAdd []string `json:"CapAdd"` CapDrop []string `json:"CapDrop"` diff --git a/cmd/podman/parse.go b/cmd/podman/parse.go index 53d49c36c..bb45d08c4 100644 --- a/cmd/podman/parse.go +++ b/cmd/podman/parse.go @@ -11,14 +11,12 @@ import ( "io/ioutil" "net" "os" - "os/user" "path" "regexp" "strconv" "strings" units "github.com/docker/go-units" - specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" ) @@ -713,77 +711,6 @@ func parseStorageOpts(storageOpts []string) (map[string]string, error) { //nolin return m, nil } -// parseUser parses the the uid and gid in the format [:] -// for user flag -// FIXME: Issue from https://github.com/projectatomic/buildah/issues/66 -func parseUser(rootdir, userspec string) (specs.User, error) { //nolint - var gid64 uint64 - var gerr error = user.UnknownGroupError("error looking up group") - - spec := strings.SplitN(userspec, ":", 2) - userspec = spec[0] - groupspec := "" - if userspec == "" { - return specs.User{}, nil - } - if len(spec) > 1 { - groupspec = spec[1] - } - - uid64, uerr := strconv.ParseUint(userspec, 10, 32) - if uerr == nil && groupspec == "" { - // We parsed the user name as a number, and there's no group - // component, so we need to look up the user's primary GID. - var name string - name, gid64, gerr = lookupGroupForUIDInContainer(rootdir, uid64) - if gerr == nil { - userspec = name - } else { - if userrec, err := user.LookupId(userspec); err == nil { - gid64, gerr = strconv.ParseUint(userrec.Gid, 10, 32) - userspec = userrec.Name - } - } - } - if uerr != nil { - uid64, gid64, uerr = lookupUserInContainer(rootdir, userspec) - gerr = uerr - } - if uerr != nil { - if userrec, err := user.Lookup(userspec); err == nil { - uid64, uerr = strconv.ParseUint(userrec.Uid, 10, 32) - gid64, gerr = strconv.ParseUint(userrec.Gid, 10, 32) - } - } - - if groupspec != "" { - gid64, gerr = strconv.ParseUint(groupspec, 10, 32) - if gerr != nil { - gid64, gerr = lookupGroupInContainer(rootdir, groupspec) - } - if gerr != nil { - if group, err := user.LookupGroup(groupspec); err == nil { - gid64, gerr = strconv.ParseUint(group.Gid, 10, 32) - } - } - } - - if uerr == nil && gerr == nil { - u := specs.User{ - UID: uint32(uid64), - GID: uint32(gid64), - Username: userspec, - } - return u, nil - } - - err := errors.Wrapf(uerr, "error determining run uid") - if uerr == nil { - err = errors.Wrapf(gerr, "error determining run gid") - } - return specs.User{}, err -} - // convertKVStringsToMap converts ["key=value"] to {"key":"value"} func convertKVStringsToMap(values []string) map[string]string { result := make(map[string]string, len(values)) diff --git a/cmd/podman/run.go b/cmd/podman/run.go index bc93459ad..654b7a47e 100644 --- a/cmd/podman/run.go +++ b/cmd/podman/run.go @@ -1,9 +1,8 @@ package main import ( + "encoding/json" "fmt" - "io" - "os" "sync" "github.com/pkg/errors" @@ -26,7 +25,6 @@ var runCommand = cli.Command{ } func runCmd(c *cli.Context) error { - var imageName string if err := validateFlags(c, createFlags); err != nil { return err } @@ -41,51 +39,10 @@ func runCmd(c *cli.Context) error { return err } - createImage := runtime.NewImage(createConfig.Image) - createImage.LocalName, _ = createImage.GetLocalImageName() - if createImage.LocalName == "" { - // The image wasnt found by the user input'd name or its fqname - // Pull the image - var writer io.Writer - if !createConfig.Quiet { - writer = os.Stdout - } - createImage.Pull(writer) - } - runtimeSpec, err := createConfigToOCISpec(createConfig) if err != nil { return err } - logrus.Debug("spec is ", runtimeSpec) - - if createImage.LocalName != "" { - nameIsID, err := runtime.IsImageID(createImage.LocalName) - if err != nil { - return err - } - if nameIsID { - // If the input from the user is an ID, then we need to get the image - // name for cstorage - createImage.LocalName, err = createImage.GetNameByID() - if err != nil { - return err - } - } - imageName = createImage.LocalName - } else { - imageName, err = createImage.GetFQName() - } - if err != nil { - return err - } - logrus.Debug("imageName is ", imageName) - - imageID, err := createImage.GetImageID() - if err != nil { - return err - } - logrus.Debug("imageID is ", imageID) options, err := createConfig.GetContainerCreateOptions() if err != nil { @@ -93,8 +50,10 @@ func runCmd(c *cli.Context) error { } // Gather up the options for NewContainer which consist of With... funcs - options = append(options, libpod.WithRootFSFromImage(imageID, imageName, false)) + options = append(options, libpod.WithRootFSFromImage(createConfig.ImageID, createConfig.Image, true)) options = append(options, libpod.WithSELinuxLabels(createConfig.ProcessLabel, createConfig.MountLabel)) + options = append(options, libpod.WithLabels(createConfig.Labels)) + options = append(options, libpod.WithUser(createConfig.User)) options = append(options, libpod.WithShmDir(createConfig.ShmDir)) ctr, err := runtime.NewContainer(runtimeSpec, options...) if err != nil { @@ -107,6 +66,16 @@ func runCmd(c *cli.Context) error { } logrus.Debugf("container storage created for %q", ctr.ID()) + createConfigJSON, err := json.Marshal(createConfig) + if err != nil { + return err + } + if err := ctr.AddArtifact("create-config", createConfigJSON); err != nil { + return err + } + + logrus.Debug("new container created ", ctr.ID()) + if c.String("cidfile") != "" { libpod.WriteFile(ctr.ID(), c.String("cidfile")) return nil diff --git a/cmd/podman/spec.go b/cmd/podman/spec.go index e000467e2..96eb2f6ee 100644 --- a/cmd/podman/spec.go +++ b/cmd/podman/spec.go @@ -193,9 +193,6 @@ func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) { g.SetProcessCwd(config.WorkDir) g.SetProcessArgs(config.Command) g.SetProcessTerminal(config.Tty) - // User and Group must go together - g.SetProcessUID(config.User) - g.SetProcessGID(config.Group) for _, gid := range config.GroupAdd { g.AddProcessAdditionalGid(gid) } diff --git a/libpod/container.go b/libpod/container.go index 454fe43ac..18f3ca5ae 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -28,6 +28,7 @@ import ( "github.com/pkg/errors" "github.com/projectatomic/libpod/libpod/driver" crioAnnotations "github.com/projectatomic/libpod/pkg/annotations" + "github.com/projectatomic/libpod/pkg/chrootuser" "github.com/sirupsen/logrus" "github.com/ulule/deepcopier" "golang.org/x/sys/unix" @@ -153,7 +154,8 @@ type ContainerConfig struct { SharedNamespaceMap map[string]string `json:"sharedNamespaces"` // Time container was created CreatedTime time.Time `json:"createdTime"` - + // User/GID to use within the container + User string `json:"user"` // TODO save log location here and pass into OCI code // TODO allow overriding of log path } @@ -440,7 +442,6 @@ func newContainer(rspec *spec.Spec, lockDir string) (*Container, error) { ctr.config.Spec = new(spec.Spec) deepcopier.Copy(rspec).To(ctr.config.Spec) - ctr.config.CreatedTime = time.Now() // Path our lock file will reside at @@ -614,6 +615,20 @@ func (c *Container) Init() (err error) { g.AddBindMount(runDirResolv, "/etc/resolv.conf", []string{"rw"}) // Bind mount hosts g.AddBindMount(runDirHosts, "/etc/hosts", []string{"rw"}) + + if c.config.User != "" { + if !c.state.Mounted { + return errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate User field", c.ID()) + } + uid, gid, err := chrootuser.GetUser(c.state.Mountpoint, c.config.User) + if err != nil { + return err + } + // User and Group must go together + g.SetProcessUID(uid) + g.SetProcessGID(gid) + } + c.runningSpec = g.Spec() c.runningSpec.Root.Path = c.state.Mountpoint c.runningSpec.Annotations[crioAnnotations.Created] = c.config.CreatedTime.Format(time.RFC3339Nano) @@ -1078,7 +1093,7 @@ func (c *Container) mountStorage() (err error) { } } - mountPoint, err := c.runtime.storageService.StartContainer(c.ID()) + mountPoint, err := c.runtime.storageService.MountContainerImage(c.ID()) if err != nil { return errors.Wrapf(err, "error mounting storage for container %s", c.ID()) } @@ -1124,7 +1139,7 @@ func (c *Container) cleanupStorage() error { } // Also unmount storage - if err := c.runtime.storageService.StopContainer(c.ID()); err != nil { + if err := c.runtime.storageService.UnmountContainerImage(c.ID()); err != nil { return errors.Wrapf(err, "error unmounting container %s root filesystem", c.ID()) } diff --git a/libpod/options.go b/libpod/options.go index 4836e1d67..70db3bdae 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -307,6 +307,19 @@ func WithSELinuxLabels(processLabel, mountLabel string) CtrCreateOption { } } +// WithUser sets the user identity field in configutation +// Valid uses [user | user:group | uid | uid:gid | user:gid | uid:group ] +func WithUser(user string) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return ErrCtrFinalized + } + + ctr.config.User = user + return nil + } +} + // WithRootFSFromImage sets up a fresh root filesystem using the given image // If useImageConfig is specified, image volumes, environment variables, and // other configuration from the image will be added to the config diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 1f2b8945e..9e42ff8d1 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -25,14 +25,14 @@ type CtrCreateOption func(*Container) error type ContainerFilter func(*Container) bool // NewContainer creates a new container from a given OCI config -func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) { +func (r *Runtime) NewContainer(rSpec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) { r.lock.Lock() defer r.lock.Unlock() if !r.valid { return nil, ErrRuntimeStopped } - ctr, err := newContainer(spec, r.lockDir) + ctr, err := newContainer(rSpec, r.lockDir) if err != nil { return nil, err } diff --git a/libpod/storage.go b/libpod/storage.go index 5e18aaf5c..42b9a86ad 100644 --- a/libpod/storage.go +++ b/libpod/storage.go @@ -200,7 +200,7 @@ func (r *storageService) GetContainerMetadata(idOrName string) (RuntimeContainer return metadata, nil } -func (r *storageService) StartContainer(idOrName string) (string, error) { +func (r *storageService) MountContainerImage(idOrName string) (string, error) { container, err := r.store.Container(idOrName) if err != nil { if errors.Cause(err) == storage.ErrContainerUnknown { @@ -221,7 +221,7 @@ func (r *storageService) StartContainer(idOrName string) (string, error) { return mountPoint, nil } -func (r *storageService) StopContainer(idOrName string) error { +func (r *storageService) UnmountContainerImage(idOrName string) error { if idOrName == "" { return ErrEmptyID } diff --git a/pkg/chrootuser/user.go b/pkg/chrootuser/user.go new file mode 100644 index 000000000..a024877a5 --- /dev/null +++ b/pkg/chrootuser/user.go @@ -0,0 +1,71 @@ +package chrootuser + +import ( + "os/user" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// GetUser will return the uid, gid of the user specified in the userspec +// it will use the /etc/password and /etc/shadow files inside of the rootdir +// to return this information. +// userspace format [user | user:group | uid | uid:gid | user:gid | uid:group ] +func GetUser(rootdir, userspec string) (uint32, uint32, error) { + var gid64 uint64 + var gerr error = user.UnknownGroupError("error looking up group") + + spec := strings.SplitN(userspec, ":", 2) + userspec = spec[0] + groupspec := "" + if userspec == "" { + return 0, 0, nil + } + if len(spec) > 1 { + groupspec = spec[1] + } + + uid64, uerr := strconv.ParseUint(userspec, 10, 32) + if uerr == nil && groupspec == "" { + // We parsed the user name as a number, and there's no group + // component, so try to look up the primary GID of the user who + // has this UID. + var name string + name, gid64, gerr = lookupGroupForUIDInContainer(rootdir, uid64) + if gerr == nil { + userspec = name + } else { + // Leave userspec alone, but swallow the error and just + // use GID 0. + gid64 = 0 + gerr = nil + } + } + if uerr != nil { + // The user ID couldn't be parsed as a number, so try to look + // up the user's UID and primary GID. + uid64, gid64, uerr = lookupUserInContainer(rootdir, userspec) + gerr = uerr + } + + if groupspec != "" { + // We have a group name or number, so parse it. + gid64, gerr = strconv.ParseUint(groupspec, 10, 32) + if gerr != nil { + // The group couldn't be parsed as a number, so look up + // the group's GID. + gid64, gerr = lookupGroupInContainer(rootdir, groupspec) + } + } + + if uerr == nil && gerr == nil { + return uint32(uid64), uint32(gid64), nil + } + + err := errors.Wrapf(uerr, "error determining run uid") + if uerr == nil { + err = errors.Wrapf(gerr, "error determining run gid") + } + return 0, 0, err +} diff --git a/pkg/chrootuser/user_basic.go b/pkg/chrootuser/user_basic.go new file mode 100644 index 000000000..4f89af557 --- /dev/null +++ b/pkg/chrootuser/user_basic.go @@ -0,0 +1,19 @@ +// +build !linux + +package chrootuser + +import ( + "github.com/pkg/errors" +) + +func lookupUserInContainer(rootdir, username string) (uint64, uint64, error) { + return 0, 0, errors.New("user lookup not supported") +} + +func lookupGroupInContainer(rootdir, groupname string) (uint64, error) { + return 0, errors.New("group lookup not supported") +} + +func lookupGroupForUIDInContainer(rootdir string, userid uint64) (string, uint64, error) { + return "", 0, errors.New("primary group lookup by uid not supported") +} diff --git a/pkg/chrootuser/user_linux.go b/pkg/chrootuser/user_linux.go new file mode 100644 index 000000000..2baf9ea33 --- /dev/null +++ b/pkg/chrootuser/user_linux.go @@ -0,0 +1,235 @@ +// +build linux + +package chrootuser + +import ( + "bufio" + "flag" + "fmt" + "io" + "os" + "os/exec" + "os/user" + "strconv" + "strings" + "sync" + + "github.com/containers/storage/pkg/reexec" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +const ( + openChrootedCommand = "chrootuser-open" +) + +func init() { + reexec.Register(openChrootedCommand, openChrootedFileMain) +} + +func openChrootedFileMain() { + status := 0 + flag.Parse() + if len(flag.Args()) < 1 { + os.Exit(1) + } + // Our first parameter is the directory to chroot into. + if err := unix.Chdir(flag.Arg(0)); err != nil { + fmt.Fprintf(os.Stderr, "chdir(): %v", err) + os.Exit(1) + } + if err := unix.Chroot(flag.Arg(0)); err != nil { + fmt.Fprintf(os.Stderr, "chroot(): %v", err) + os.Exit(1) + } + // Anything else is a file we want to dump out. + for _, filename := range flag.Args()[1:] { + f, err := os.Open(filename) + if err != nil { + fmt.Fprintf(os.Stderr, "open(%q): %v", filename, err) + status = 1 + continue + } + _, err = io.Copy(os.Stdout, f) + if err != nil { + fmt.Fprintf(os.Stderr, "read(%q): %v", filename, err) + } + f.Close() + } + os.Exit(status) +} + +func openChrootedFile(rootdir, filename string) (*exec.Cmd, io.ReadCloser, error) { + // The child process expects a chroot and one or more filenames that + // will be consulted relative to the chroot directory and concatenated + // to its stdout. Start it up. + cmd := reexec.Command(openChrootedCommand, rootdir, filename) + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, nil, err + } + err = cmd.Start() + if err != nil { + return nil, nil, err + } + // Hand back the child's stdout for reading, and the child to reap. + return cmd, stdout, nil +} + +var ( + lookupUser, lookupGroup sync.Mutex +) + +type lookupPasswdEntry struct { + name string + uid uint64 + gid uint64 +} +type lookupGroupEntry struct { + name string + gid uint64 +} + +func readWholeLine(rc *bufio.Reader) ([]byte, error) { + line, isPrefix, err := rc.ReadLine() + if err != nil { + return nil, err + } + for isPrefix { + // We didn't get a whole line. Keep reading chunks until we find an end of line, and discard them. + for isPrefix { + logrus.Debugf("discarding partial line %q", string(line)) + _, isPrefix, err = rc.ReadLine() + if err != nil { + return nil, err + } + } + // That last read was the end of a line, so now we try to read the (beginning of?) the next line. + line, isPrefix, err = rc.ReadLine() + if err != nil { + return nil, err + } + } + return line, nil +} + +func parseNextPasswd(rc *bufio.Reader) *lookupPasswdEntry { + line, err := readWholeLine(rc) + if err != nil { + return nil + } + fields := strings.Split(string(line), ":") + if len(fields) < 7 { + return nil + } + uid, err := strconv.ParseUint(fields[2], 10, 32) + if err != nil { + return nil + } + gid, err := strconv.ParseUint(fields[3], 10, 32) + if err != nil { + return nil + } + return &lookupPasswdEntry{ + name: fields[0], + uid: uid, + gid: gid, + } +} + +func parseNextGroup(rc *bufio.Reader) *lookupGroupEntry { + line, err := readWholeLine(rc) + if err != nil { + return nil + } + fields := strings.Split(string(line), ":") + if len(fields) < 4 { + return nil + } + gid, err := strconv.ParseUint(fields[2], 10, 32) + if err != nil { + return nil + } + return &lookupGroupEntry{ + name: fields[0], + gid: gid, + } +} + +func lookupUserInContainer(rootdir, username string) (uid uint64, gid uint64, err error) { + cmd, f, err := openChrootedFile(rootdir, "/etc/passwd") + if err != nil { + return 0, 0, err + } + defer func() { + _ = cmd.Wait() + }() + rc := bufio.NewReader(f) + defer f.Close() + + lookupUser.Lock() + defer lookupUser.Unlock() + + pwd := parseNextPasswd(rc) + for pwd != nil { + if pwd.name != username { + pwd = parseNextPasswd(rc) + continue + } + return pwd.uid, pwd.gid, nil + } + + return 0, 0, user.UnknownUserError(fmt.Sprintf("error looking up user %q", username)) +} + +func lookupGroupForUIDInContainer(rootdir string, userid uint64) (username string, gid uint64, err error) { + cmd, f, err := openChrootedFile(rootdir, "/etc/passwd") + if err != nil { + return "", 0, err + } + defer func() { + _ = cmd.Wait() + }() + rc := bufio.NewReader(f) + defer f.Close() + + lookupUser.Lock() + defer lookupUser.Unlock() + + pwd := parseNextPasswd(rc) + for pwd != nil { + if pwd.uid != userid { + pwd = parseNextPasswd(rc) + continue + } + return pwd.name, pwd.gid, nil + } + + return "", 0, user.UnknownUserError(fmt.Sprintf("error looking up user with UID %d", userid)) +} + +func lookupGroupInContainer(rootdir, groupname string) (gid uint64, err error) { + cmd, f, err := openChrootedFile(rootdir, "/etc/group") + if err != nil { + return 0, err + } + defer func() { + _ = cmd.Wait() + }() + rc := bufio.NewReader(f) + defer f.Close() + + lookupGroup.Lock() + defer lookupGroup.Unlock() + + grp := parseNextGroup(rc) + for grp != nil { + if grp.name != groupname { + grp = parseNextGroup(rc) + continue + } + return grp.gid, nil + } + + return 0, user.UnknownGroupError(fmt.Sprintf("error looking up group %q", groupname)) +} -- cgit v1.2.3-54-g00ecf