diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/adapter/runtime.go | 93 | ||||
-rw-r--r-- | pkg/adapter/runtime_remote.go | 75 | ||||
-rw-r--r-- | pkg/rootless/rootless.go | 9 | ||||
-rw-r--r-- | pkg/rootless/rootless_linux.go | 96 | ||||
-rw-r--r-- | pkg/rootless/rootless_unsupported.go | 33 | ||||
-rw-r--r-- | pkg/spec/spec.go | 9 | ||||
-rw-r--r-- | pkg/util/utils.go | 23 | ||||
-rw-r--r-- | pkg/varlinkapi/events.go | 56 | ||||
-rw-r--r-- | pkg/varlinkapi/system.go | 1 |
9 files changed, 328 insertions, 67 deletions
diff --git a/pkg/adapter/runtime.go b/pkg/adapter/runtime.go index 732b89530..a0951f677 100644 --- a/pkg/adapter/runtime.go +++ b/pkg/adapter/runtime.go @@ -3,11 +3,13 @@ package adapter import ( + "bufio" "context" "io" "io/ioutil" "os" "strconv" + "text/template" "github.com/containers/buildah" "github.com/containers/buildah/imagebuildah" @@ -16,7 +18,9 @@ import ( "github.com/containers/image/types" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/rootless" "github.com/pkg/errors" @@ -337,3 +341,92 @@ func IsImageNotFound(err error) bool { func (r *LocalRuntime) HealthCheck(c *cliconfig.HealthCheckValues) (libpod.HealthCheckStatus, error) { return r.Runtime.HealthCheck(c.InputArgs[0]) } + +// JoinOrCreateRootlessPod joins the specified pod if it is running or it creates a new user namespace +// if the pod is stopped +func (r *LocalRuntime) JoinOrCreateRootlessPod(pod *Pod) (bool, int, error) { + if os.Geteuid() == 0 { + return false, 0, nil + } + opts := rootless.Opts{ + Argument: pod.ID(), + } + + inspect, err := pod.Inspect() + if err != nil { + return false, 0, err + } + for _, ctr := range inspect.Containers { + prevCtr, err := r.LookupContainer(ctr.ID) + if err != nil { + return false, -1, err + } + s, err := prevCtr.State() + if err != nil { + return false, -1, err + } + if s != libpod.ContainerStateRunning && s != libpod.ContainerStatePaused { + continue + } + data, err := ioutil.ReadFile(prevCtr.Config().ConmonPidFile) + if err != nil { + return false, -1, errors.Wrapf(err, "cannot read conmon PID file %q", prevCtr.Config().ConmonPidFile) + } + conmonPid, err := strconv.Atoi(string(data)) + if err != nil { + return false, -1, errors.Wrapf(err, "cannot parse PID %q", data) + } + return rootless.JoinDirectUserAndMountNSWithOpts(uint(conmonPid), &opts) + } + + return rootless.BecomeRootInUserNSWithOpts(&opts) +} + +// Events is a wrapper to libpod to obtain libpod/podman events +func (r *LocalRuntime) Events(c *cliconfig.EventValues) error { + var ( + fromStart bool + eventsError error + ) + options, err := shared.GenerateEventOptions(c.Filter, c.Since, c.Until) + if err != nil { + return errors.Wrapf(err, "unable to generate event options") + } + tmpl, err := template.New("events").Parse(c.Format) + if err != nil { + return err + } + if len(c.Since) > 0 || len(c.Until) > 0 { + fromStart = true + } + eventChannel := make(chan *events.Event) + go func() { + eventsError = r.Runtime.Events(fromStart, c.Stream, options, eventChannel) + }() + + if eventsError != nil { + return eventsError + } + if err != nil { + return errors.Wrapf(err, "unable to tail the events log") + } + w := bufio.NewWriter(os.Stdout) + for event := range eventChannel { + if len(c.Format) > 0 { + if err := tmpl.Execute(w, event); err != nil { + return err + } + } else { + if _, err := w.Write([]byte(event.ToHumanReadable())); err != nil { + return err + } + } + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + if err := w.Flush(); err != nil { + return err + } + } + return nil +} diff --git a/pkg/adapter/runtime_remote.go b/pkg/adapter/runtime_remote.go index 10c25c3f3..01f774dbd 100644 --- a/pkg/adapter/runtime_remote.go +++ b/pkg/adapter/runtime_remote.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "os" "strings" + "text/template" "time" "github.com/containers/buildah/imagebuildah" @@ -18,6 +19,7 @@ import ( "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/utils" "github.com/containers/storage/pkg/archive" @@ -751,3 +753,76 @@ func IsImageNotFound(err error) bool { func (r *LocalRuntime) HealthCheck(c *cliconfig.HealthCheckValues) (libpod.HealthCheckStatus, error) { return -1, libpod.ErrNotImplemented } + +// JoinOrCreateRootlessPod joins the specified pod if it is running or it creates a new user namespace +// if the pod is stopped +func (r *LocalRuntime) JoinOrCreateRootlessPod(pod *Pod) (bool, int, error) { + // Nothing to do in the remote case + return true, 0, nil +} + +// Events monitors libpod/podman events over a varlink connection +func (r *LocalRuntime) Events(c *cliconfig.EventValues) error { + reply, err := iopodman.GetEvents().Send(r.Conn, uint64(varlink.More), c.Filter, c.Since, c.Stream, c.Until) + if err != nil { + return errors.Wrapf(err, "unable to obtain events") + } + + w := bufio.NewWriter(os.Stdout) + tmpl, err := template.New("events").Parse(c.Format) + if err != nil { + return err + } + + for { + returnedEvent, flags, err := reply() + if err != nil { + // When the error handling is back into podman, we can flip this to a better way to check + // for problems. For now, this works. + return err + } + if returnedEvent.Time == "" && returnedEvent.Status == "" && returnedEvent.Type == "" { + // We got a blank event return, signals end of stream in certain cases + break + } + eTime, err := time.Parse(time.RFC3339Nano, returnedEvent.Time) + if err != nil { + return errors.Wrapf(err, "unable to parse time of event %s", returnedEvent.Time) + } + eType, err := events.StringToType(returnedEvent.Type) + if err != nil { + return err + } + eStatus, err := events.StringToStatus(returnedEvent.Status) + if err != nil { + return err + } + event := events.Event{ + ID: returnedEvent.Id, + Image: returnedEvent.Image, + Name: returnedEvent.Name, + Status: eStatus, + Time: eTime, + Type: eType, + } + if len(c.Format) > 0 { + if err := tmpl.Execute(w, event); err != nil { + return err + } + } else { + if _, err := w.Write([]byte(event.ToHumanReadable())); err != nil { + return err + } + } + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + if err := w.Flush(); err != nil { + return err + } + if flags&varlink.Continues == 0 { + break + } + } + return nil +} diff --git a/pkg/rootless/rootless.go b/pkg/rootless/rootless.go new file mode 100644 index 000000000..a531e43ce --- /dev/null +++ b/pkg/rootless/rootless.go @@ -0,0 +1,9 @@ +package rootless + +// Opts allows to customize how re-execing to a rootless process is done +type Opts struct { + // Argument overrides the arguments on the command line + // for the re-execed process. The process in the namespace + // must use rootless.Argument() to read its value. + Argument string +} diff --git a/pkg/rootless/rootless_linux.go b/pkg/rootless/rootless_linux.go index 933cfa2c2..b2677f7d9 100644 --- a/pkg/rootless/rootless_linux.go +++ b/pkg/rootless/rootless_linux.go @@ -11,7 +11,6 @@ import ( "os/user" "runtime" "strconv" - "strings" "sync" "syscall" "unsafe" @@ -61,6 +60,11 @@ func SkipStorageSetup() bool { return skipStorageSetup } +// Argument returns the argument that was set for the rootless session. +func Argument() string { + return os.Getenv("_LIBPOD_ROOTLESS_ARG") +} + // GetRootlessUID returns the UID of the user in the parent userNS func GetRootlessUID() int { uidEnv := os.Getenv("_LIBPOD_ROOTLESS_UID") @@ -135,8 +139,16 @@ func JoinNS(pid uint, preserveFDs int) (bool, int, error) { // JoinDirectUserAndMountNS re-exec podman in a new userNS and join the user and mount // namespace of the specified PID without looking up its parent. Useful to join directly -// the conmon process. +// the conmon process. It is a convenience function for JoinDirectUserAndMountNSWithOpts +// with a default configuration. func JoinDirectUserAndMountNS(pid uint) (bool, int, error) { + return JoinDirectUserAndMountNSWithOpts(pid, nil) +} + +// JoinDirectUserAndMountNSWithOpts re-exec podman in a new userNS and join the user and +// mount namespace of the specified PID without looking up its parent. Useful to join +// directly the conmon process. +func JoinDirectUserAndMountNSWithOpts(pid uint, opts *Opts) (bool, int, error) { if os.Geteuid() == 0 || os.Getenv("_LIBPOD_USERNS_CONFIGURED") != "" { return false, -1, nil } @@ -153,6 +165,12 @@ func JoinDirectUserAndMountNS(pid uint) (bool, int, error) { } defer userNS.Close() + if opts != nil && opts.Argument != "" { + if err := os.Setenv("_LIBPOD_ROOTLESS_ARG", opts.Argument); err != nil { + return false, -1, err + } + } + pidC := C.reexec_userns_join(C.int(userNS.Fd()), C.int(mountNS.Fd())) if int(pidC) < 0 { return false, -1, errors.Errorf("cannot re-exec process") @@ -192,27 +210,19 @@ func JoinNSPath(path string) (bool, int, error) { return true, int(ret), nil } -const defaultMinimumMappings = 65536 - -func getMinimumIDs(p string) int { - content, err := ioutil.ReadFile(p) - if err != nil { - logrus.Debugf("error reading data from %q, use a default value of %d", p, defaultMinimumMappings) - return defaultMinimumMappings - } - ret, err := strconv.Atoi(strings.TrimSuffix(string(content), "\n")) - if err != nil { - logrus.Debugf("error reading data from %q, use a default value of %d", p, defaultMinimumMappings) - return defaultMinimumMappings - } - return ret + 1 -} - // BecomeRootInUserNS re-exec podman in a new userNS. It returns whether podman was re-executed // into a new user namespace and the return code from the re-executed podman process. // If podman was re-executed the caller needs to propagate the error code returned by the child -// process. +// process. It is a convenience function for BecomeRootInUserNSWithOpts with a default configuration. func BecomeRootInUserNS() (bool, int, error) { + return BecomeRootInUserNSWithOpts(nil) +} + +// BecomeRootInUserNSWithOpts re-exec podman in a new userNS. It returns whether podman was +// re-execute into a new user namespace and the return code from the re-executed podman process. +// If podman was re-executed the caller needs to propagate the error code returned by the child +// process. +func BecomeRootInUserNSWithOpts(opts *Opts) (bool, int, error) { if os.Geteuid() == 0 || os.Getenv("_LIBPOD_USERNS_CONFIGURED") != "" { if os.Getenv("_LIBPOD_USERNS_CONFIGURED") == "init" { return false, 0, runInUser() @@ -231,53 +241,30 @@ func BecomeRootInUserNS() (bool, int, error) { defer w.Close() defer w.Write([]byte("0")) + if opts != nil && opts.Argument != "" { + if err := os.Setenv("_LIBPOD_ROOTLESS_ARG", opts.Argument); err != nil { + return false, -1, err + } + } + pidC := C.reexec_in_user_namespace(C.int(r.Fd())) pid := int(pidC) if pid < 0 { return false, -1, errors.Errorf("cannot re-exec process") } - allowSingleIDMapping := os.Getenv("PODMAN_ALLOW_SINGLE_ID_MAPPING_IN_USERNS") != "" - var uids, gids []idtools.IDMap username := os.Getenv("USER") if username == "" { user, err := user.LookupId(fmt.Sprintf("%d", os.Getuid())) - if err != nil && !allowSingleIDMapping { - if os.IsNotExist(err) { - return false, 0, errors.Wrapf(err, "/etc/subuid or /etc/subgid does not exist, see subuid/subgid man pages for information on these files") - } - return false, 0, errors.Wrapf(err, "could not find user by UID nor USER env was set") - } if err == nil { username = user.Username } } mappings, err := idtools.NewIDMappings(username, username) - if !allowSingleIDMapping { - if err != nil { - return false, -1, err - } - - availableGIDs, availableUIDs := 0, 0 - for _, i := range mappings.UIDs() { - availableUIDs += i.Size - } - - minUIDs := getMinimumIDs("/proc/sys/kernel/overflowuid") - if availableUIDs < minUIDs { - return false, 0, fmt.Errorf("not enough UIDs available for the user, at least %d are needed", minUIDs) - } - - for _, i := range mappings.GIDs() { - availableGIDs += i.Size - } - minGIDs := getMinimumIDs("/proc/sys/kernel/overflowgid") - if availableGIDs < minGIDs { - return false, 0, fmt.Errorf("not enough GIDs available for the user, at least %d are needed", minGIDs) - } - } - if err == nil { + if err != nil { + logrus.Warnf("cannot find mappings for user %s: %v", username, err) + } else { uids = mappings.UIDs() gids = mappings.GIDs() } @@ -285,12 +272,10 @@ func BecomeRootInUserNS() (bool, int, error) { uidsMapped := false if mappings != nil && uids != nil { err := tryMappingTool("newuidmap", pid, os.Getuid(), uids) - if !allowSingleIDMapping && err != nil { - return false, 0, err - } uidsMapped = err == nil } if !uidsMapped { + logrus.Warnf("using rootless single mapping into the namespace. This might break some images. Check /etc/subuid and /etc/subgid for adding subids") setgroups := fmt.Sprintf("/proc/%d/setgroups", pid) err = ioutil.WriteFile(setgroups, []byte("deny\n"), 0666) if err != nil { @@ -307,9 +292,6 @@ func BecomeRootInUserNS() (bool, int, error) { gidsMapped := false if mappings != nil && gids != nil { err := tryMappingTool("newgidmap", pid, os.Getgid(), gids) - if !allowSingleIDMapping && err != nil { - return false, 0, err - } gidsMapped = err == nil } if !gidsMapped { diff --git a/pkg/rootless/rootless_unsupported.go b/pkg/rootless/rootless_unsupported.go index 1823c023e..54e70961b 100644 --- a/pkg/rootless/rootless_unsupported.go +++ b/pkg/rootless/rootless_unsupported.go @@ -11,10 +11,18 @@ func IsRootless() bool { return false } +// BecomeRootInUserNS re-exec podman in a new userNS. It returns whether podman was re-executed +// into a new user namespace and the return code from the re-executed podman process. +// If podman was re-executed the caller needs to propagate the error code returned by the child +// process. It is a convenience function for BecomeRootInUserNSWithOpts with a default configuration. +func BecomeRootInUserNS() (bool, int, error) { + return false, -1, errors.New("this function is not supported on this os") +} + // BecomeRootInUserNS is a stub function that always returns false and an // error on unsupported OS's -func BecomeRootInUserNS() (bool, int, error) { - return false, -1, errors.New("this function is not supported on this os1") +func BecomeRootInUserNSWithOpts(opts *Opts) (bool, int, error) { + return false, -1, errors.New("this function is not supported on this os") } // GetRootlessUID returns the UID of the user in the parent userNS @@ -34,18 +42,31 @@ func SkipStorageSetup() bool { // JoinNS re-exec podman in a new userNS and join the user namespace of the specified // PID. func JoinNS(pid uint) (bool, int, error) { - return false, -1, errors.New("this function is not supported on this os2") + return false, -1, errors.New("this function is not supported on this os") } // JoinNSPath re-exec podman in a new userNS and join the owner user namespace of the // specified path. func JoinNSPath(path string) (bool, int, error) { - return false, -1, errors.New("this function is not supported on this os3") + return false, -1, errors.New("this function is not supported on this os") +} + +// JoinDirectUserAndMountNSWithOpts re-exec podman in a new userNS and join the user and +// mount namespace of the specified PID without looking up its parent. Useful to join +// directly the conmon process. +func JoinDirectUserAndMountNSWithOpts(pid uint, opts *Opts) (bool, int, error) { + return false, -1, errors.New("this function is not supported on this os") } // JoinDirectUserAndMountNS re-exec podman in a new userNS and join the user and mount // namespace of the specified PID without looking up its parent. Useful to join directly -// the conmon process. +// the conmon process. It is a convenience function for JoinDirectUserAndMountNSWithOpts +// with a default configuration. func JoinDirectUserAndMountNS(pid uint) (bool, int, error) { - return false, -1, errors.New("this function is not supported on this os4") + return false, -1, errors.New("this function is not supported on this os") +} + +// Argument returns the argument that was set for the rootless session. +func Argument() string { + return "" } diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 28a636fa6..32d47732b 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -454,10 +454,6 @@ func findMount(target string, mounts []*pmount.Info) (*pmount.Info, error) { } func blockAccessToKernelFilesystems(config *CreateConfig, g *generate.Generator) { - if config.PidMode.IsHost() && rootless.IsRootless() { - return - } - if !config.Privileged { for _, mp := range []string{ "/proc/acpi", @@ -469,10 +465,15 @@ func blockAccessToKernelFilesystems(config *CreateConfig, g *generate.Generator) "/proc/sched_debug", "/proc/scsi", "/sys/firmware", + "/sys/fs/selinux", } { g.AddLinuxMaskedPaths(mp) } + if config.PidMode.IsHost() && rootless.IsRootless() { + return + } + for _, rp := range []string{ "/proc/asound", "/proc/bus", diff --git a/pkg/util/utils.go b/pkg/util/utils.go index a4576191b..d7e1ddd38 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "syscall" + "time" "github.com/BurntSushi/toml" "github.com/containers/image/types" @@ -347,3 +348,25 @@ func StorageConfigFile() string { } return storage.DefaultConfigFile } + +// ParseInputTime takes the users input and to determine if it is valid and +// returns a time format and error. The input is compared to known time formats +// or a duration which implies no-duration +func ParseInputTime(inputTime string) (time.Time, error) { + timeFormats := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05", "2006-01-02T15:04:05.999999999", + "2006-01-02Z07:00", "2006-01-02"} + // iterate the supported time formats + for _, tf := range timeFormats { + t, err := time.Parse(tf, inputTime) + if err == nil { + return t, nil + } + } + + // input might be a duration + duration, err := time.ParseDuration(inputTime) + if err != nil { + return time.Time{}, errors.Errorf("unable to interpret time value") + } + return time.Now().Add(-duration), nil +} diff --git a/pkg/varlinkapi/events.go b/pkg/varlinkapi/events.go new file mode 100644 index 000000000..d3fe3d65f --- /dev/null +++ b/pkg/varlinkapi/events.go @@ -0,0 +1,56 @@ +package varlinkapi + +import ( + "fmt" + "time" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/cmd/podman/varlink" + "github.com/containers/libpod/libpod/events" +) + +// GetEvents is a remote endpoint to get events from the event log +func (i *LibpodAPI) GetEvents(call iopodman.VarlinkCall, filter []string, since string, stream bool, until string) error { + var ( + fromStart bool + eventsError error + event *events.Event + ) + if call.WantsMore() { + call.Continues = true + } + filters, err := shared.GenerateEventOptions(filter, since, until) + if err != nil { + return call.ReplyErrorOccurred(err.Error()) + } + if len(since) > 0 || len(until) > 0 { + fromStart = true + } + eventChannel := make(chan *events.Event) + go func() { + eventsError = i.Runtime.Events(fromStart, stream, filters, eventChannel) + }() + if eventsError != nil { + return call.ReplyErrorOccurred(err.Error()) + } + for { + event = <-eventChannel + if event == nil { + call.Continues = false + break + } + call.ReplyGetEvents(iopodman.Event{ + Id: event.ID, + Image: event.Image, + Name: event.Name, + Status: fmt.Sprintf("%s", event.Status), + Time: event.Time.Format(time.RFC3339Nano), + Type: fmt.Sprintf("%s", event.Type), + }) + if !call.Continues { + // For a one-shot on events, we break out here + break + } + } + return call.ReplyGetEvents(iopodman.Event{}) +} diff --git a/pkg/varlinkapi/system.go b/pkg/varlinkapi/system.go index 3f32615ec..816143e9f 100644 --- a/pkg/varlinkapi/system.go +++ b/pkg/varlinkapi/system.go @@ -52,6 +52,7 @@ func (i *LibpodAPI) GetInfo(call iopodman.VarlinkCall) error { Mem_free: host["MemFree"].(int64), Mem_total: host["MemTotal"].(int64), Swap_free: host["SwapFree"].(int64), + Swap_total: host["SwapTotal"].(int64), Arch: host["arch"].(string), Cpus: int64(host["cpus"].(int)), Hostname: host["hostname"].(string), |