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 <dwalsh@redhat.com>

Closes: #110
Approved by: mheon
This commit is contained in:
Daniel J Walsh
2017-12-18 12:05:06 -05:00
committed by Atomic Bot
parent 8aeb38e4a7
commit 00d38cb379
12 changed files with 553 additions and 205 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/signal"
"github.com/docker/go-connections/nat"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/opencontainers/selinux/go-selinux/label" "github.com/opencontainers/selinux/go-selinux/label"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -80,10 +81,11 @@ type createConfig struct {
DNSServers []string //dns DNSServers []string //dns
Entrypoint string //entrypoint Entrypoint string //entrypoint
Env map[string]string //env Env map[string]string //env
Expose []string //expose ExposedPorts map[nat.Port]struct{}
GroupAdd []uint32 // group-add GroupAdd []uint32 // group-add
Hostname string //hostname Hostname string //hostname
Image string Image string
ImageID string
Interactive bool //interactive Interactive bool //interactive
IpcMode container.IpcMode //ipc IpcMode container.IpcMode //ipc
IP6Address string //ipv6 IP6Address string //ipv6
@ -99,7 +101,8 @@ type createConfig struct {
NetworkAlias []string //network-alias NetworkAlias []string //network-alias
PidMode container.PidMode //pid PidMode container.PidMode //pid
NsUser string NsUser string
Pod string //pod Pod string //pod
PortBindings nat.PortMap
Privileged bool //privileged Privileged bool //privileged
Publish []string //publish Publish []string //publish
PublishAll bool //publish-all PublishAll bool //publish-all
@ -115,8 +118,7 @@ type createConfig struct {
Sysctl map[string]string //sysctl Sysctl map[string]string //sysctl
Tmpfs []string // tmpfs Tmpfs []string // tmpfs
Tty bool //tty Tty bool //tty
User uint32 //user User string //user
Group uint32 // group
UtsMode container.UTSMode //uts UtsMode container.UTSMode //uts
Volumes []string //volume Volumes []string //volume
WorkDir string //workdir WorkDir string //workdir
@ -148,7 +150,6 @@ var createCommand = cli.Command{
func createCmd(c *cli.Context) error { func createCmd(c *cli.Context) error {
// TODO should allow user to create based off a directory on the host not just image // TODO should allow user to create based off a directory on the host not just image
// Need CLI support for this // Need CLI support for this
var imageName string
if err := validateFlags(c, createFlags); err != nil { if err := validateFlags(c, createFlags); err != nil {
return err return err
} }
@ -164,54 +165,19 @@ func createCmd(c *cli.Context) error {
return err 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) runtimeSpec, err := createConfigToOCISpec(createConfig)
if err != nil { if err != nil {
return err 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() options, err := createConfig.GetContainerCreateOptions()
if err != nil { if err != nil {
return errors.Wrapf(err, "unable to parse new container options") return errors.Wrapf(err, "unable to parse new container options")
} }
// Gather up the options for NewContainer which consist of With... funcs // 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.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)) options = append(options, libpod.WithShmDir(createConfig.ShmDir))
ctr, err := runtime.NewContainer(runtimeSpec, options...) ctr, err := runtime.NewContainer(runtimeSpec, options...)
if err != nil { if err != nil {
@ -300,13 +266,101 @@ func parseSecurityOpt(config *createConfig, securityOpts []string) error {
return err 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 <portnum>/[<proto>] or <startport-endport>/[<proto>]
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 // Parses CLI options related to container creation into a config which can be
// parsed into an OCI runtime spec // parsed into an OCI runtime spec
func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, error) { func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, error) {
var command []string var command []string
var memoryLimit, memoryReservation, memorySwap, memoryKernel int64 var memoryLimit, memoryReservation, memorySwap, memoryKernel int64
var blkioWeight uint16 var blkioWeight uint16
var uid, gid uint32
if len(c.Args()) < 1 { if len(c.Args()) < 1 {
return nil, errors.Errorf("image name or ID is required") 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:] 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"), "=") sysctl, err := convertStringSliceToMap(c.StringSlice("sysctl"), "=")
if err != nil { 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")) groupAdd, err := stringSlicetoUint32Slice(c.StringSlice("group-add"))
if err != nil { if err != nil {
return &createConfig{}, errors.Wrapf(err, "invalid value for groups provided") return nil, 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
} }
if c.String("memory") != "" { if c.String("memory") != "" {
@ -417,14 +452,79 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er
} }
shmDir = ctr.ShmDir() 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") { 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 { if err != nil {
return nil, err 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{ config := &createConfig{
Runtime: runtime, Runtime: runtime,
CapAdd: c.StringSlice("cap-add"), CapAdd: c.StringSlice("cap-add"),
@ -436,12 +536,13 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er
DNSOpt: c.StringSlice("dns-opt"), DNSOpt: c.StringSlice("dns-opt"),
DNSSearch: c.StringSlice("dns-search"), DNSSearch: c.StringSlice("dns-search"),
DNSServers: c.StringSlice("dns"), DNSServers: c.StringSlice("dns"),
Entrypoint: c.String("entrypoint"), Entrypoint: entrypoint,
Env: env, Env: env,
Expose: c.StringSlice("expose"), ExposedPorts: ports,
GroupAdd: groupAdd, GroupAdd: groupAdd,
Hostname: c.String("hostname"), Hostname: c.String("hostname"),
Image: image, Image: imageName,
ImageID: imageID,
Interactive: c.Bool("interactive"), Interactive: c.Bool("interactive"),
IP6Address: c.String("ipv6"), IP6Address: c.String("ipv6"),
IPAddress: c.String("ip"), IPAddress: c.String("ip"),
@ -461,6 +562,7 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er
Privileged: c.Bool("privileged"), Privileged: c.Bool("privileged"),
Publish: c.StringSlice("publish"), Publish: c.StringSlice("publish"),
PublishAll: c.Bool("publish-all"), PublishAll: c.Bool("publish-all"),
PortBindings: portBindings,
Quiet: c.Bool("quiet"), Quiet: c.Bool("quiet"),
ReadOnlyRootfs: c.Bool("read-only"), ReadOnlyRootfs: c.Bool("read-only"),
Resources: createResourceConfig{ Resources: createResourceConfig{
@ -499,10 +601,9 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er
Sysctl: sysctl, Sysctl: sysctl,
Tmpfs: c.StringSlice("tmpfs"), Tmpfs: c.StringSlice("tmpfs"),
Tty: tty, Tty: tty,
User: uid, User: user,
Group: gid,
Volumes: c.StringSlice("volume"), Volumes: c.StringSlice("volume"),
WorkDir: c.String("workdir"), WorkDir: workDir,
} }
if !config.Privileged { if !config.Privileged {

View File

@ -3,6 +3,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"github.com/docker/go-connections/nat"
specs "github.com/opencontainers/runtime-spec/specs-go" specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/projectatomic/libpod/cmd/podman/formats" "github.com/projectatomic/libpod/cmd/podman/formats"
@ -287,7 +288,7 @@ type HostConfig struct {
ContainerIDFile string `json:"ContainerIDFile"` ContainerIDFile string `json:"ContainerIDFile"`
LogConfig *LogConfig `json:"LogConfig"` //TODO LogConfig *LogConfig `json:"LogConfig"` //TODO
NetworkMode string `json:"NetworkMode"` NetworkMode string `json:"NetworkMode"`
PortBindings map[string]struct{} `json:"PortBindings"` //TODO PortBindings nat.PortMap `json:"PortBindings"` //TODO
AutoRemove bool `json:"AutoRemove"` AutoRemove bool `json:"AutoRemove"`
CapAdd []string `json:"CapAdd"` CapAdd []string `json:"CapAdd"`
CapDrop []string `json:"CapDrop"` CapDrop []string `json:"CapDrop"`

View File

@ -11,14 +11,12 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
"os/user"
"path" "path"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
units "github.com/docker/go-units" units "github.com/docker/go-units"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors" "github.com/pkg/errors"
pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" 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 return m, nil
} }
// parseUser parses the the uid and gid in the format <name|uid>[:<group|gid>]
// 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"} // convertKVStringsToMap converts ["key=value"] to {"key":"value"}
func convertKVStringsToMap(values []string) map[string]string { func convertKVStringsToMap(values []string) map[string]string {
result := make(map[string]string, len(values)) result := make(map[string]string, len(values))

View File

@ -1,9 +1,8 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"io"
"os"
"sync" "sync"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -26,7 +25,6 @@ var runCommand = cli.Command{
} }
func runCmd(c *cli.Context) error { func runCmd(c *cli.Context) error {
var imageName string
if err := validateFlags(c, createFlags); err != nil { if err := validateFlags(c, createFlags); err != nil {
return err return err
} }
@ -41,51 +39,10 @@ func runCmd(c *cli.Context) error {
return err 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) runtimeSpec, err := createConfigToOCISpec(createConfig)
if err != nil { if err != nil {
return err 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() options, err := createConfig.GetContainerCreateOptions()
if err != nil { if err != nil {
@ -93,8 +50,10 @@ func runCmd(c *cli.Context) error {
} }
// Gather up the options for NewContainer which consist of With... funcs // 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.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)) options = append(options, libpod.WithShmDir(createConfig.ShmDir))
ctr, err := runtime.NewContainer(runtimeSpec, options...) ctr, err := runtime.NewContainer(runtimeSpec, options...)
if err != nil { if err != nil {
@ -107,6 +66,16 @@ func runCmd(c *cli.Context) error {
} }
logrus.Debugf("container storage created for %q", ctr.ID()) 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") != "" { if c.String("cidfile") != "" {
libpod.WriteFile(ctr.ID(), c.String("cidfile")) libpod.WriteFile(ctr.ID(), c.String("cidfile"))
return nil return nil

View File

@ -193,9 +193,6 @@ func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) {
g.SetProcessCwd(config.WorkDir) g.SetProcessCwd(config.WorkDir)
g.SetProcessArgs(config.Command) g.SetProcessArgs(config.Command)
g.SetProcessTerminal(config.Tty) g.SetProcessTerminal(config.Tty)
// User and Group must go together
g.SetProcessUID(config.User)
g.SetProcessGID(config.Group)
for _, gid := range config.GroupAdd { for _, gid := range config.GroupAdd {
g.AddProcessAdditionalGid(gid) g.AddProcessAdditionalGid(gid)
} }

View File

@ -28,6 +28,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/projectatomic/libpod/libpod/driver" "github.com/projectatomic/libpod/libpod/driver"
crioAnnotations "github.com/projectatomic/libpod/pkg/annotations" crioAnnotations "github.com/projectatomic/libpod/pkg/annotations"
"github.com/projectatomic/libpod/pkg/chrootuser"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/ulule/deepcopier" "github.com/ulule/deepcopier"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@ -153,7 +154,8 @@ type ContainerConfig struct {
SharedNamespaceMap map[string]string `json:"sharedNamespaces"` SharedNamespaceMap map[string]string `json:"sharedNamespaces"`
// Time container was created // Time container was created
CreatedTime time.Time `json:"createdTime"` 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 save log location here and pass into OCI code
// TODO allow overriding of log path // 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) ctr.config.Spec = new(spec.Spec)
deepcopier.Copy(rspec).To(ctr.config.Spec) deepcopier.Copy(rspec).To(ctr.config.Spec)
ctr.config.CreatedTime = time.Now() ctr.config.CreatedTime = time.Now()
// Path our lock file will reside at // 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"}) g.AddBindMount(runDirResolv, "/etc/resolv.conf", []string{"rw"})
// Bind mount hosts // Bind mount hosts
g.AddBindMount(runDirHosts, "/etc/hosts", []string{"rw"}) 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 = g.Spec()
c.runningSpec.Root.Path = c.state.Mountpoint c.runningSpec.Root.Path = c.state.Mountpoint
c.runningSpec.Annotations[crioAnnotations.Created] = c.config.CreatedTime.Format(time.RFC3339Nano) 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 { if err != nil {
return errors.Wrapf(err, "error mounting storage for container %s", c.ID()) return errors.Wrapf(err, "error mounting storage for container %s", c.ID())
} }
@ -1124,7 +1139,7 @@ func (c *Container) cleanupStorage() error {
} }
// Also unmount storage // 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()) return errors.Wrapf(err, "error unmounting container %s root filesystem", c.ID())
} }

View File

@ -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 // WithRootFSFromImage sets up a fresh root filesystem using the given image
// If useImageConfig is specified, image volumes, environment variables, and // If useImageConfig is specified, image volumes, environment variables, and
// other configuration from the image will be added to the config // other configuration from the image will be added to the config

View File

@ -25,14 +25,14 @@ type CtrCreateOption func(*Container) error
type ContainerFilter func(*Container) bool type ContainerFilter func(*Container) bool
// NewContainer creates a new container from a given OCI config // 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() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
if !r.valid { if !r.valid {
return nil, ErrRuntimeStopped return nil, ErrRuntimeStopped
} }
ctr, err := newContainer(spec, r.lockDir) ctr, err := newContainer(rSpec, r.lockDir)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -200,7 +200,7 @@ func (r *storageService) GetContainerMetadata(idOrName string) (RuntimeContainer
return metadata, nil 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) container, err := r.store.Container(idOrName)
if err != nil { if err != nil {
if errors.Cause(err) == storage.ErrContainerUnknown { if errors.Cause(err) == storage.ErrContainerUnknown {
@ -221,7 +221,7 @@ func (r *storageService) StartContainer(idOrName string) (string, error) {
return mountPoint, nil return mountPoint, nil
} }
func (r *storageService) StopContainer(idOrName string) error { func (r *storageService) UnmountContainerImage(idOrName string) error {
if idOrName == "" { if idOrName == "" {
return ErrEmptyID return ErrEmptyID
} }

71
pkg/chrootuser/user.go Normal file
View File

@ -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
}

View File

@ -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")
}

View File

@ -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))
}