608 lines
17 KiB
Go
608 lines
17 KiB
Go
// +build !windows
|
|
|
|
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package oci
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/containerd/containerd/containers"
|
|
"github.com/containerd/containerd/content"
|
|
"github.com/containerd/containerd/images"
|
|
"github.com/containerd/containerd/mount"
|
|
"github.com/containerd/containerd/namespaces"
|
|
"github.com/containerd/continuity/fs"
|
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/opencontainers/runc/libcontainer/user"
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/syndtr/gocapability/capability"
|
|
)
|
|
|
|
// WithTTY sets the information on the spec as well as the environment variables for
|
|
// using a TTY
|
|
func WithTTY(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setProcess(s)
|
|
s.Process.Terminal = true
|
|
s.Process.Env = append(s.Process.Env, "TERM=xterm")
|
|
return nil
|
|
}
|
|
|
|
// setRoot sets Root to empty if unset
|
|
func setRoot(s *specs.Spec) {
|
|
if s.Root == nil {
|
|
s.Root = &specs.Root{}
|
|
}
|
|
}
|
|
|
|
// setLinux sets Linux to empty if unset
|
|
func setLinux(s *specs.Spec) {
|
|
if s.Linux == nil {
|
|
s.Linux = &specs.Linux{}
|
|
}
|
|
}
|
|
|
|
// setCapabilities sets Linux Capabilities to empty if unset
|
|
func setCapabilities(s *specs.Spec) {
|
|
setProcess(s)
|
|
if s.Process.Capabilities == nil {
|
|
s.Process.Capabilities = &specs.LinuxCapabilities{}
|
|
}
|
|
}
|
|
|
|
// WithHostNamespace allows a task to run inside the host's linux namespace
|
|
func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setLinux(s)
|
|
for i, n := range s.Linux.Namespaces {
|
|
if n.Type == ns {
|
|
s.Linux.Namespaces = append(s.Linux.Namespaces[:i], s.Linux.Namespaces[i+1:]...)
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithLinuxNamespace uses the passed in namespace for the spec. If a namespace of the same type already exists in the
|
|
// spec, the existing namespace is replaced by the one provided.
|
|
func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setLinux(s)
|
|
for i, n := range s.Linux.Namespaces {
|
|
if n.Type == ns.Type {
|
|
before := s.Linux.Namespaces[:i]
|
|
after := s.Linux.Namespaces[i+1:]
|
|
s.Linux.Namespaces = append(before, ns)
|
|
s.Linux.Namespaces = append(s.Linux.Namespaces, after...)
|
|
return nil
|
|
}
|
|
}
|
|
s.Linux.Namespaces = append(s.Linux.Namespaces, ns)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithImageConfig configures the spec to from the configuration of an Image
|
|
func WithImageConfig(image Image) SpecOpts {
|
|
return func(ctx context.Context, client Client, c *containers.Container, s *specs.Spec) error {
|
|
ic, err := image.Config(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var (
|
|
ociimage v1.Image
|
|
config v1.ImageConfig
|
|
)
|
|
switch ic.MediaType {
|
|
case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
|
|
p, err := content.ReadBlob(ctx, image.ContentStore(), ic.Digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := json.Unmarshal(p, &ociimage); err != nil {
|
|
return err
|
|
}
|
|
config = ociimage.Config
|
|
default:
|
|
return fmt.Errorf("unknown image config media type %s", ic.MediaType)
|
|
}
|
|
|
|
setProcess(s)
|
|
s.Process.Env = append(s.Process.Env, config.Env...)
|
|
cmd := config.Cmd
|
|
s.Process.Args = append(config.Entrypoint, cmd...)
|
|
cwd := config.WorkingDir
|
|
if cwd == "" {
|
|
cwd = "/"
|
|
}
|
|
s.Process.Cwd = cwd
|
|
if config.User != "" {
|
|
return WithUser(config.User)(ctx, client, c, s)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithRootFSPath specifies unmanaged rootfs path.
|
|
func WithRootFSPath(path string) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setRoot(s)
|
|
s.Root.Path = path
|
|
// Entrypoint is not set here (it's up to caller)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithRootFSReadonly sets specs.Root.Readonly to true
|
|
func WithRootFSReadonly() SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setRoot(s)
|
|
s.Root.Readonly = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithNoNewPrivileges sets no_new_privileges on the process for the container
|
|
func WithNoNewPrivileges(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setProcess(s)
|
|
s.Process.NoNewPrivileges = true
|
|
return nil
|
|
}
|
|
|
|
// WithHostHostsFile bind-mounts the host's /etc/hosts into the container as readonly
|
|
func WithHostHostsFile(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
s.Mounts = append(s.Mounts, specs.Mount{
|
|
Destination: "/etc/hosts",
|
|
Type: "bind",
|
|
Source: "/etc/hosts",
|
|
Options: []string{"rbind", "ro"},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// WithHostResolvconf bind-mounts the host's /etc/resolv.conf into the container as readonly
|
|
func WithHostResolvconf(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
s.Mounts = append(s.Mounts, specs.Mount{
|
|
Destination: "/etc/resolv.conf",
|
|
Type: "bind",
|
|
Source: "/etc/resolv.conf",
|
|
Options: []string{"rbind", "ro"},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// WithHostLocaltime bind-mounts the host's /etc/localtime into the container as readonly
|
|
func WithHostLocaltime(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
s.Mounts = append(s.Mounts, specs.Mount{
|
|
Destination: "/etc/localtime",
|
|
Type: "bind",
|
|
Source: "/etc/localtime",
|
|
Options: []string{"rbind", "ro"},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// WithUserNamespace sets the uid and gid mappings for the task
|
|
// this can be called multiple times to add more mappings to the generated spec
|
|
func WithUserNamespace(container, host, size uint32) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
var hasUserns bool
|
|
setLinux(s)
|
|
for _, ns := range s.Linux.Namespaces {
|
|
if ns.Type == specs.UserNamespace {
|
|
hasUserns = true
|
|
break
|
|
}
|
|
}
|
|
if !hasUserns {
|
|
s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{
|
|
Type: specs.UserNamespace,
|
|
})
|
|
}
|
|
mapping := specs.LinuxIDMapping{
|
|
ContainerID: container,
|
|
HostID: host,
|
|
Size: size,
|
|
}
|
|
s.Linux.UIDMappings = append(s.Linux.UIDMappings, mapping)
|
|
s.Linux.GIDMappings = append(s.Linux.GIDMappings, mapping)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithCgroup sets the container's cgroup path
|
|
func WithCgroup(path string) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setLinux(s)
|
|
s.Linux.CgroupsPath = path
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithNamespacedCgroup uses the namespace set on the context to create a
|
|
// root directory for containers in the cgroup with the id as the subcgroup
|
|
func WithNamespacedCgroup() SpecOpts {
|
|
return func(ctx context.Context, _ Client, c *containers.Container, s *specs.Spec) error {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
setLinux(s)
|
|
s.Linux.CgroupsPath = filepath.Join("/", namespace, c.ID)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithUser sets the user to be used within the container.
|
|
// It accepts a valid user string in OCI Image Spec v1.0.0:
|
|
// user, uid, user:group, uid:gid, uid:group, user:gid
|
|
func WithUser(userstr string) SpecOpts {
|
|
return func(ctx context.Context, client Client, c *containers.Container, s *specs.Spec) error {
|
|
setProcess(s)
|
|
parts := strings.Split(userstr, ":")
|
|
switch len(parts) {
|
|
case 1:
|
|
v, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
// if we cannot parse as a uint they try to see if it is a username
|
|
return WithUsername(userstr)(ctx, client, c, s)
|
|
}
|
|
return WithUserID(uint32(v))(ctx, client, c, s)
|
|
case 2:
|
|
var (
|
|
username string
|
|
groupname string
|
|
)
|
|
var uid, gid uint32
|
|
v, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
username = parts[0]
|
|
} else {
|
|
uid = uint32(v)
|
|
}
|
|
if v, err = strconv.Atoi(parts[1]); err != nil {
|
|
groupname = parts[1]
|
|
} else {
|
|
gid = uint32(v)
|
|
}
|
|
if username == "" && groupname == "" {
|
|
s.Process.User.UID, s.Process.User.GID = uid, gid
|
|
return nil
|
|
}
|
|
f := func(root string) error {
|
|
if username != "" {
|
|
uid, _, err = getUIDGIDFromPath(root, func(u user.User) bool {
|
|
return u.Name == username
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if groupname != "" {
|
|
gid, err = getGIDFromPath(root, func(g user.Group) bool {
|
|
return g.Name == groupname
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
s.Process.User.UID, s.Process.User.GID = uid, gid
|
|
return nil
|
|
}
|
|
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
|
if !isRootfsAbs(s.Root.Path) {
|
|
return errors.New("rootfs absolute path is required")
|
|
}
|
|
return f(s.Root.Path)
|
|
}
|
|
if c.Snapshotter == "" {
|
|
return errors.New("no snapshotter set for container")
|
|
}
|
|
if c.SnapshotKey == "" {
|
|
return errors.New("rootfs snapshot not created for container")
|
|
}
|
|
snapshotter := client.SnapshotService(c.Snapshotter)
|
|
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return mount.WithTempMount(ctx, mounts, f)
|
|
default:
|
|
return fmt.Errorf("invalid USER value %s", userstr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithUIDGID allows the UID and GID for the Process to be set
|
|
func WithUIDGID(uid, gid uint32) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setProcess(s)
|
|
s.Process.User.UID = uid
|
|
s.Process.User.GID = gid
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithUserID sets the correct UID and GID for the container based
|
|
// on the image's /etc/passwd contents. If /etc/passwd does not exist,
|
|
// or uid is not found in /etc/passwd, it sets gid to be the same with
|
|
// uid, and not returns error.
|
|
func WithUserID(uid uint32) SpecOpts {
|
|
return func(ctx context.Context, client Client, c *containers.Container, s *specs.Spec) (err error) {
|
|
setProcess(s)
|
|
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
|
if !isRootfsAbs(s.Root.Path) {
|
|
return errors.Errorf("rootfs absolute path is required")
|
|
}
|
|
uuid, ugid, err := getUIDGIDFromPath(s.Root.Path, func(u user.User) bool {
|
|
return u.Uid == int(uid)
|
|
})
|
|
if err != nil {
|
|
if os.IsNotExist(err) || err == errNoUsersFound {
|
|
s.Process.User.UID, s.Process.User.GID = uid, uid
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
s.Process.User.UID, s.Process.User.GID = uuid, ugid
|
|
return nil
|
|
|
|
}
|
|
if c.Snapshotter == "" {
|
|
return errors.Errorf("no snapshotter set for container")
|
|
}
|
|
if c.SnapshotKey == "" {
|
|
return errors.Errorf("rootfs snapshot not created for container")
|
|
}
|
|
snapshotter := client.SnapshotService(c.Snapshotter)
|
|
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return mount.WithTempMount(ctx, mounts, func(root string) error {
|
|
uuid, ugid, err := getUIDGIDFromPath(root, func(u user.User) bool {
|
|
return u.Uid == int(uid)
|
|
})
|
|
if err != nil {
|
|
if os.IsNotExist(err) || err == errNoUsersFound {
|
|
s.Process.User.UID, s.Process.User.GID = uid, uid
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
s.Process.User.UID, s.Process.User.GID = uuid, ugid
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
// WithUsername sets the correct UID and GID for the container
|
|
// based on the the image's /etc/passwd contents. If /etc/passwd
|
|
// does not exist, or the username is not found in /etc/passwd,
|
|
// it returns error.
|
|
func WithUsername(username string) SpecOpts {
|
|
return func(ctx context.Context, client Client, c *containers.Container, s *specs.Spec) (err error) {
|
|
setProcess(s)
|
|
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
|
if !isRootfsAbs(s.Root.Path) {
|
|
return errors.Errorf("rootfs absolute path is required")
|
|
}
|
|
uid, gid, err := getUIDGIDFromPath(s.Root.Path, func(u user.User) bool {
|
|
return u.Name == username
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.Process.User.UID, s.Process.User.GID = uid, gid
|
|
return nil
|
|
}
|
|
if c.Snapshotter == "" {
|
|
return errors.Errorf("no snapshotter set for container")
|
|
}
|
|
if c.SnapshotKey == "" {
|
|
return errors.Errorf("rootfs snapshot not created for container")
|
|
}
|
|
snapshotter := client.SnapshotService(c.Snapshotter)
|
|
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return mount.WithTempMount(ctx, mounts, func(root string) error {
|
|
uid, gid, err := getUIDGIDFromPath(root, func(u user.User) bool {
|
|
return u.Name == username
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.Process.User.UID, s.Process.User.GID = uid, gid
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
// WithCapabilities sets Linux capabilities on the process
|
|
func WithCapabilities(caps []string) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setCapabilities(s)
|
|
|
|
s.Process.Capabilities.Bounding = caps
|
|
s.Process.Capabilities.Effective = caps
|
|
s.Process.Capabilities.Permitted = caps
|
|
s.Process.Capabilities.Inheritable = caps
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithAllCapabilities sets all linux capabilities for the process
|
|
var WithAllCapabilities = WithCapabilities(getAllCapabilities())
|
|
|
|
func getAllCapabilities() []string {
|
|
last := capability.CAP_LAST_CAP
|
|
// hack for RHEL6 which has no /proc/sys/kernel/cap_last_cap
|
|
if last == capability.Cap(63) {
|
|
last = capability.CAP_BLOCK_SUSPEND
|
|
}
|
|
var caps []string
|
|
for _, cap := range capability.List() {
|
|
if cap > last {
|
|
continue
|
|
}
|
|
caps = append(caps, "CAP_"+strings.ToUpper(cap.String()))
|
|
}
|
|
return caps
|
|
}
|
|
|
|
var errNoUsersFound = errors.New("no users found")
|
|
|
|
func getUIDGIDFromPath(root string, filter func(user.User) bool) (uid, gid uint32, err error) {
|
|
ppath, err := fs.RootPath(root, "/etc/passwd")
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
users, err := user.ParsePasswdFileFilter(ppath, filter)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if len(users) == 0 {
|
|
return 0, 0, errNoUsersFound
|
|
}
|
|
u := users[0]
|
|
return uint32(u.Uid), uint32(u.Gid), nil
|
|
}
|
|
|
|
var errNoGroupsFound = errors.New("no groups found")
|
|
|
|
func getGIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err error) {
|
|
gpath, err := fs.RootPath(root, "/etc/group")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
groups, err := user.ParseGroupFileFilter(gpath, filter)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(groups) == 0 {
|
|
return 0, errNoGroupsFound
|
|
}
|
|
g := groups[0]
|
|
return uint32(g.Gid), nil
|
|
}
|
|
|
|
func isRootfsAbs(root string) bool {
|
|
return filepath.IsAbs(root)
|
|
}
|
|
|
|
// WithMaskedPaths sets the masked paths option
|
|
func WithMaskedPaths(paths []string) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setLinux(s)
|
|
s.Linux.MaskedPaths = paths
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithReadonlyPaths sets the read only paths option
|
|
func WithReadonlyPaths(paths []string) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setLinux(s)
|
|
s.Linux.ReadonlyPaths = paths
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithWriteableSysfs makes any sysfs mounts writeable
|
|
func WithWriteableSysfs(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
for i, m := range s.Mounts {
|
|
if m.Type == "sysfs" {
|
|
var options []string
|
|
for _, o := range m.Options {
|
|
if o == "ro" {
|
|
o = "rw"
|
|
}
|
|
options = append(options, o)
|
|
}
|
|
s.Mounts[i].Options = options
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WithWriteableCgroupfs makes any cgroup mounts writeable
|
|
func WithWriteableCgroupfs(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
for i, m := range s.Mounts {
|
|
if m.Type == "cgroup" {
|
|
var options []string
|
|
for _, o := range m.Options {
|
|
if o == "ro" {
|
|
o = "rw"
|
|
}
|
|
options = append(options, o)
|
|
}
|
|
s.Mounts[i].Options = options
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WithSelinuxLabel sets the process SELinux label
|
|
func WithSelinuxLabel(label string) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setProcess(s)
|
|
s.Process.SelinuxLabel = label
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithApparmorProfile sets the Apparmor profile for the process
|
|
func WithApparmorProfile(profile string) SpecOpts {
|
|
return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setProcess(s)
|
|
s.Process.ApparmorProfile = profile
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithSeccompUnconfined clears the seccomp profile
|
|
func WithSeccompUnconfined(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error {
|
|
setLinux(s)
|
|
s.Linux.Seccomp = nil
|
|
return nil
|
|
}
|
|
|
|
// WithPrivileged sets up options for a privileged container
|
|
// TODO(justincormack) device handling
|
|
var WithPrivileged = Compose(
|
|
WithAllCapabilities,
|
|
WithMaskedPaths(nil),
|
|
WithReadonlyPaths(nil),
|
|
WithWriteableSysfs,
|
|
WithWriteableCgroupfs,
|
|
WithSelinuxLabel(""),
|
|
WithApparmorProfile(""),
|
|
WithSeccompUnconfined,
|
|
)
|