Merge pull request #2592 from jterry75/specs_with_platform

Merges the oci package for Linux and Windows
This commit is contained in:
Phil Estes 2018-08-30 11:34:49 -07:00 committed by GitHub
commit 777cc50c72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1078 additions and 1143 deletions

View File

@ -22,29 +22,12 @@ import (
"github.com/containerd/console" "github.com/containerd/console"
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/oci" "github.com/containerd/containerd/oci"
specs "github.com/opencontainers/runtime-spec/specs-go" specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
func withTTY(terminal bool) oci.SpecOpts {
if !terminal {
return func(ctx gocontext.Context, client oci.Client, c *containers.Container, s *specs.Spec) error {
s.Process.Terminal = false
return nil
}
}
con := console.Current()
size, err := con.Size()
if err != nil {
logrus.WithError(err).Error("console size")
}
return oci.WithTTY(int(size.Width), int(size.Height))
}
// NewContainer creates a new container // NewContainer creates a new container
func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) { func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) {
var ( var (
@ -73,7 +56,17 @@ func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli
opts = append(opts, oci.WithImageConfig(image)) opts = append(opts, oci.WithImageConfig(image))
opts = append(opts, oci.WithEnv(context.StringSlice("env"))) opts = append(opts, oci.WithEnv(context.StringSlice("env")))
opts = append(opts, withMounts(context)) opts = append(opts, withMounts(context))
opts = append(opts, withTTY(context.Bool("tty"))) if context.Bool("tty") {
opts = append(opts, oci.WithTTY)
con := console.Current()
size, err := con.Size()
if err != nil {
logrus.WithError(err).Error("console size")
}
opts = append(opts, oci.WithTTYSize(int(size.Width), int(size.Height)))
}
if len(args) > 0 { if len(args) > 0 {
opts = append(opts, oci.WithProcessArgs(args...)) opts = append(opts, oci.WithProcessArgs(args...))
} }

View File

@ -18,11 +18,26 @@ package oci
import ( import (
"context" "context"
"path/filepath"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/containers" "github.com/containerd/containerd/containers"
specs "github.com/opencontainers/runtime-spec/specs-go" specs "github.com/opencontainers/runtime-spec/specs-go"
) )
const (
rwm = "rwm"
defaultRootfsPath = "rootfs"
)
var (
defaultUnixEnv = []string{
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
}
)
// Spec is a type alias to the OCI runtime spec to allow third part SpecOpts // Spec is a type alias to the OCI runtime spec to allow third part SpecOpts
// to be created without the "issues" with go vendoring and package imports // to be created without the "issues" with go vendoring and package imports
type Spec = specs.Spec type Spec = specs.Spec
@ -30,12 +45,28 @@ type Spec = specs.Spec
// GenerateSpec will generate a default spec from the provided image // GenerateSpec will generate a default spec from the provided image
// for use as a containerd container // for use as a containerd container
func GenerateSpec(ctx context.Context, client Client, c *containers.Container, opts ...SpecOpts) (*Spec, error) { func GenerateSpec(ctx context.Context, client Client, c *containers.Container, opts ...SpecOpts) (*Spec, error) {
s, err := createDefaultSpec(ctx, c.ID) return GenerateSpecWithPlatform(ctx, client, platforms.DefaultString(), c, opts...)
}
// GenerateSpecWithPlatform will generate a default spec from the provided image
// for use as a containerd container in the platform requested.
func GenerateSpecWithPlatform(ctx context.Context, client Client, platform string, c *containers.Container, opts ...SpecOpts) (*Spec, error) {
plat, err := platforms.Parse(platform)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s, ApplyOpts(ctx, client, c, s, opts...) var s Spec
if plat.OS == "windows" {
err = populateDefaultWindowsSpec(ctx, &s, c.ID)
} else {
err = populateDefaultUnixSpec(ctx, &s, c.ID)
}
if err != nil {
return nil, err
}
return &s, ApplyOpts(ctx, client, c, &s, opts...)
} }
// ApplyOpts applys the options to the given spec, injecting data from the // ApplyOpts applys the options to the given spec, injecting data from the
@ -50,7 +81,173 @@ func ApplyOpts(ctx context.Context, client Client, c *containers.Container, s *S
return nil return nil
} }
func createDefaultSpec(ctx context.Context, id string) (*Spec, error) { func defaultUnixCaps() []string {
var s Spec return []string{
return &s, populateDefaultSpec(ctx, &s, id) "CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETFCAP",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_KILL",
"CAP_AUDIT_WRITE",
}
}
func defaultUnixNamespaces() []specs.LinuxNamespace {
return []specs.LinuxNamespace{
{
Type: specs.PIDNamespace,
},
{
Type: specs.IPCNamespace,
},
{
Type: specs.UTSNamespace,
},
{
Type: specs.MountNamespace,
},
{
Type: specs.NetworkNamespace,
},
}
}
func populateDefaultUnixSpec(ctx context.Context, s *Spec, id string) error {
ns, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return err
}
*s = Spec{
Version: specs.Version,
Root: &specs.Root{
Path: defaultRootfsPath,
},
Process: &specs.Process{
Env: defaultUnixEnv,
Cwd: "/",
NoNewPrivileges: true,
User: specs.User{
UID: 0,
GID: 0,
},
Capabilities: &specs.LinuxCapabilities{
Bounding: defaultUnixCaps(),
Permitted: defaultUnixCaps(),
Inheritable: defaultUnixCaps(),
Effective: defaultUnixCaps(),
},
Rlimits: []specs.POSIXRlimit{
{
Type: "RLIMIT_NOFILE",
Hard: uint64(1024),
Soft: uint64(1024),
},
},
},
Mounts: []specs.Mount{
{
Destination: "/proc",
Type: "proc",
Source: "proc",
},
{
Destination: "/dev",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
{
Destination: "/dev/pts",
Type: "devpts",
Source: "devpts",
Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"},
},
{
Destination: "/dev/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
},
{
Destination: "/dev/mqueue",
Type: "mqueue",
Source: "mqueue",
Options: []string{"nosuid", "noexec", "nodev"},
},
{
Destination: "/sys",
Type: "sysfs",
Source: "sysfs",
Options: []string{"nosuid", "noexec", "nodev", "ro"},
},
{
Destination: "/run",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
},
Linux: &specs.Linux{
MaskedPaths: []string{
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/proc/scsi",
},
ReadonlyPaths: []string{
"/proc/asound",
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger",
},
CgroupsPath: filepath.Join("/", ns, id),
Resources: &specs.LinuxResources{
Devices: []specs.LinuxDeviceCgroup{
{
Allow: false,
Access: rwm,
},
},
},
Namespaces: defaultUnixNamespaces(),
},
}
return nil
}
func populateDefaultWindowsSpec(ctx context.Context, s *Spec, id string) error {
*s = Spec{
Version: specs.Version,
Root: &specs.Root{},
Process: &specs.Process{
Cwd: `C:\`,
ConsoleSize: &specs.Box{
Width: 80,
Height: 20,
},
},
Windows: &specs.Windows{
IgnoreFlushesDuringBoot: true,
Network: &specs.WindowsNetwork{
AllowUnqualifiedDNSQuery: true,
},
},
}
return nil
} }

View File

@ -19,12 +19,26 @@ package oci
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"os"
"path/filepath"
"runtime"
"strconv"
"strings" "strings"
"github.com/containerd/containerd/containers" "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/containerd/platforms"
"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" specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/syndtr/gocapability/capability"
) )
// SpecOpts sets spec specific information to a newly generated OCI spec // SpecOpts sets spec specific information to a newly generated OCI spec
@ -49,13 +63,56 @@ func setProcess(s *Spec) {
} }
} }
// setRoot sets Root to empty if unset
func setRoot(s *Spec) {
if s.Root == nil {
s.Root = &specs.Root{}
}
}
// setLinux sets Linux to empty if unset
func setLinux(s *Spec) {
if s.Linux == nil {
s.Linux = &specs.Linux{}
}
}
// setCapabilities sets Linux Capabilities to empty if unset
func setCapabilities(s *Spec) {
setProcess(s)
if s.Process.Capabilities == nil {
s.Process.Capabilities = &specs.LinuxCapabilities{}
}
}
// WithDefaultSpec returns a SpecOpts that will populate the spec with default // WithDefaultSpec returns a SpecOpts that will populate the spec with default
// values. // values.
// //
// Use as the first option to clear the spec, then apply options afterwards. // Use as the first option to clear the spec, then apply options afterwards.
func WithDefaultSpec() SpecOpts { func WithDefaultSpec() SpecOpts {
return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error { return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error {
return populateDefaultSpec(ctx, s, c.ID) if runtime.GOOS == "windows" {
return populateDefaultWindowsSpec(ctx, s, c.ID)
}
return populateDefaultUnixSpec(ctx, s, c.ID)
}
}
// WithDefaultSpecForPlatform returns a SpecOpts that will populate the spec
// with default values for a given platform.
//
// Use as the first option to clear the spec, then apply options afterwards.
func WithDefaultSpecForPlatform(platform string) SpecOpts {
return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error {
plat, err := platforms.Parse(platform)
if err != nil {
return err
}
if plat.OS == "windows" {
return populateDefaultWindowsSpec(ctx, s, c.ID)
}
return populateDefaultUnixSpec(ctx, s, c.ID)
} }
} }
@ -81,32 +138,6 @@ func WithSpecFromFile(filename string) SpecOpts {
} }
} }
// WithProcessArgs replaces the args on the generated spec
func WithProcessArgs(args ...string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setProcess(s)
s.Process.Args = args
return nil
}
}
// WithProcessCwd replaces the current working directory on the generated spec
func WithProcessCwd(cwd string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setProcess(s)
s.Process.Cwd = cwd
return nil
}
}
// WithHostname sets the container's hostname
func WithHostname(name string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
s.Hostname = name
return nil
}
}
// WithEnv appends environment variables // WithEnv appends environment variables
func WithEnv(environmentVariables []string) SpecOpts { func WithEnv(environmentVariables []string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
@ -118,14 +149,6 @@ func WithEnv(environmentVariables []string) SpecOpts {
} }
} }
// WithMounts appends mounts
func WithMounts(mounts []specs.Mount) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
s.Mounts = append(s.Mounts, mounts...)
return nil
}
}
// replaceOrAppendEnvValues returns the defaults with the overrides either // replaceOrAppendEnvValues returns the defaults with the overrides either
// replaced by env key or appended to the list // replaced by env key or appended to the list
func replaceOrAppendEnvValues(defaults, overrides []string) []string { func replaceOrAppendEnvValues(defaults, overrides []string) []string {
@ -163,3 +186,741 @@ func replaceOrAppendEnvValues(defaults, overrides []string) []string {
return defaults return defaults
} }
// WithProcessArgs replaces the args on the generated spec
func WithProcessArgs(args ...string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setProcess(s)
s.Process.Args = args
return nil
}
}
// WithProcessCwd replaces the current working directory on the generated spec
func WithProcessCwd(cwd string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setProcess(s)
s.Process.Cwd = cwd
return nil
}
}
// 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 *Spec) error {
setProcess(s)
s.Process.Terminal = true
if s.Linux != nil {
s.Process.Env = append(s.Process.Env, "TERM=xterm")
}
return nil
}
// WithTTYSize sets the information on the spec as well as the environment variables for
// using a TTY
func WithTTYSize(width, height int) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setProcess(s)
if s.Process.ConsoleSize == nil {
s.Process.ConsoleSize = &specs.Box{}
}
s.Process.ConsoleSize.Width = uint(width)
s.Process.ConsoleSize.Height = uint(height)
return nil
}
}
// WithHostname sets the container's hostname
func WithHostname(name string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
s.Hostname = name
return nil
}
}
// WithMounts appends mounts
func WithMounts(mounts []specs.Mount) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
s.Mounts = append(s.Mounts, mounts...)
return nil
}
}
// 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 *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 *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 WithImageConfigArgs(image, nil)
}
// WithImageConfigArgs configures the spec to from the configuration of an Image with additional args that
// replaces the CMD of the image
func WithImageConfigArgs(image Image, args []string) SpecOpts {
return func(ctx context.Context, client Client, c *containers.Container, s *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)
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)
if s.Linux != nil {
s.Process.Env = append(s.Process.Env, config.Env...)
cmd := config.Cmd
if len(args) > 0 {
cmd = args
}
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)
}
} else if s.Windows != nil {
s.Process.Env = config.Env
s.Process.Args = append(config.Entrypoint, config.Cmd...)
s.Process.User = specs.User{
Username: config.User,
}
} else {
return errors.New("spec does not contain Linux or Windows section")
}
return nil
}
}
// WithRootFSPath specifies unmanaged rootfs path.
func WithRootFSPath(path string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *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 *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 *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 *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 *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 *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 *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 *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 *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 *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 *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 the requested uid,
// additionally sets the gid to 0, and does not return an error.
func WithUserID(uid uint32) SpecOpts {
return func(ctx context.Context, client Client, c *containers.Container, s *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, 0
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, 0
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 *Spec) (err error) {
setProcess(s)
if s.Linux != nil {
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
})
} else if s.Windows != nil {
s.Process.User.Username = username
} else {
return errors.New("spec does not contain Linux or Windows section")
}
return nil
}
}
// WithCapabilities sets Linux capabilities on the process
func WithCapabilities(caps []string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *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
}
// WithAmbientCapabilities set the Linux ambient capabilities for the process
// Ambient capabilities should only be set for non-root users or the caller should
// understand how these capabilities are used and set
func WithAmbientCapabilities(caps []string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setCapabilities(s)
s.Process.Capabilities.Ambient = caps
return nil
}
}
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 *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 *Spec) error {
setLinux(s)
s.Linux.ReadonlyPaths = paths
return nil
}
}
// WithWriteableSysfs makes any sysfs mounts writeable
func WithWriteableSysfs(_ context.Context, _ Client, _ *containers.Container, s *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 *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 *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 *Spec) error {
setProcess(s)
s.Process.ApparmorProfile = profile
return nil
}
}
// WithSeccompUnconfined clears the seccomp profile
func WithSeccompUnconfined(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setLinux(s)
s.Linux.Seccomp = nil
return nil
}
// WithParentCgroupDevices uses the default cgroup setup to inherit the container's parent cgroup's
// allowed and denied devices
func WithParentCgroupDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setLinux(s)
if s.Linux.Resources == nil {
s.Linux.Resources = &specs.LinuxResources{}
}
s.Linux.Resources.Devices = nil
return nil
}
// WithDefaultUnixDevices adds the default devices for unix such as /dev/null, /dev/random to
// the container's resource cgroup spec
func WithDefaultUnixDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setLinux(s)
if s.Linux.Resources == nil {
s.Linux.Resources = &specs.LinuxResources{}
}
intptr := func(i int64) *int64 {
return &i
}
s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, []specs.LinuxDeviceCgroup{
{
// "/dev/null",
Type: "c",
Major: intptr(1),
Minor: intptr(3),
Access: rwm,
Allow: true,
},
{
// "/dev/random",
Type: "c",
Major: intptr(1),
Minor: intptr(8),
Access: rwm,
Allow: true,
},
{
// "/dev/full",
Type: "c",
Major: intptr(1),
Minor: intptr(7),
Access: rwm,
Allow: true,
},
{
// "/dev/tty",
Type: "c",
Major: intptr(5),
Minor: intptr(0),
Access: rwm,
Allow: true,
},
{
// "/dev/zero",
Type: "c",
Major: intptr(1),
Minor: intptr(5),
Access: rwm,
Allow: true,
},
{
// "/dev/urandom",
Type: "c",
Major: intptr(1),
Minor: intptr(9),
Access: rwm,
Allow: true,
},
{
// "/dev/console",
Type: "c",
Major: intptr(5),
Minor: intptr(1),
Access: rwm,
Allow: true,
},
// /dev/pts/ - pts namespaces are "coming soon"
{
Type: "c",
Major: intptr(136),
Access: rwm,
Allow: true,
},
{
Type: "c",
Major: intptr(5),
Minor: intptr(2),
Access: rwm,
Allow: true,
},
{
// tuntap
Type: "c",
Major: intptr(10),
Minor: intptr(200),
Access: rwm,
Allow: true,
},
}...)
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,
)

View File

@ -23,6 +23,7 @@ import (
"log" "log"
"os" "os"
"reflect" "reflect"
"runtime"
"testing" "testing"
"github.com/containerd/containerd/containers" "github.com/containerd/containerd/containers"
@ -108,7 +109,13 @@ func TestWithDefaultSpec(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expected, err := createDefaultSpec(ctx, c.ID) var expected Spec
var err error
if runtime.GOOS == "windows" {
err = populateDefaultWindowsSpec(ctx, &expected, c.ID)
} else {
err = populateDefaultUnixSpec(ctx, &expected, c.ID)
}
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -117,7 +124,7 @@ func TestWithDefaultSpec(t *testing.T) {
t.Fatalf("spec should not be empty") t.Fatalf("spec should not be empty")
} }
if !reflect.DeepEqual(&s, expected) { if !reflect.DeepEqual(&s, &expected) {
t.Fatalf("spec from option differs from default: \n%#v != \n%#v", &s, expected) t.Fatalf("spec from option differs from default: \n%#v != \n%#v", &s, expected)
} }
} }
@ -134,12 +141,12 @@ func TestWithSpecFromFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer fp.Close()
defer func() { defer func() {
if err := os.Remove(fp.Name()); err != nil { if err := os.Remove(fp.Name()); err != nil {
log.Printf("failed to remove tempfile %v: %v", fp.Name(), err) log.Printf("failed to remove tempfile %v: %v", fp.Name(), err)
} }
}() }()
defer fp.Close()
expected, err := GenerateSpec(ctx, nil, &c) expected, err := GenerateSpec(ctx, nil, &c)
if err != nil { if err != nil {

View File

@ -1,733 +0,0 @@
// +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 *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 *Spec) {
if s.Root == nil {
s.Root = &specs.Root{}
}
}
// setLinux sets Linux to empty if unset
func setLinux(s *Spec) {
if s.Linux == nil {
s.Linux = &specs.Linux{}
}
}
// setCapabilities sets Linux Capabilities to empty if unset
func setCapabilities(s *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 *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 *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 WithImageConfigArgs(image, nil)
}
// WithImageConfigArgs configures the spec to from the configuration of an Image with additional args that
// replaces the CMD of the image
func WithImageConfigArgs(image Image, args []string) SpecOpts {
return func(ctx context.Context, client Client, c *containers.Container, s *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)
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
if len(args) > 0 {
cmd = args
}
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 *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 *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 *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 *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 *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 *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 *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 *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 *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 *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 *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 the requested uid,
// additionally sets the gid to 0, and does not return an error.
func WithUserID(uid uint32) SpecOpts {
return func(ctx context.Context, client Client, c *containers.Container, s *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, 0
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, 0
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 *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 *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
}
// WithAmbientCapabilities set the Linux ambient capabilities for the process
// Ambient capabilities should only be set for non-root users or the caller should
// understand how these capabilities are used and set
func WithAmbientCapabilities(caps []string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setCapabilities(s)
s.Process.Capabilities.Ambient = caps
return nil
}
}
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 *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 *Spec) error {
setLinux(s)
s.Linux.ReadonlyPaths = paths
return nil
}
}
// WithWriteableSysfs makes any sysfs mounts writeable
func WithWriteableSysfs(_ context.Context, _ Client, _ *containers.Container, s *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 *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 *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 *Spec) error {
setProcess(s)
s.Process.ApparmorProfile = profile
return nil
}
}
// WithSeccompUnconfined clears the seccomp profile
func WithSeccompUnconfined(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setLinux(s)
s.Linux.Seccomp = nil
return nil
}
// WithParentCgroupDevices uses the default cgroup setup to inherit the container's parent cgroup's
// allowed and denied devices
func WithParentCgroupDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setLinux(s)
if s.Linux.Resources == nil {
s.Linux.Resources = &specs.LinuxResources{}
}
s.Linux.Resources.Devices = nil
return nil
}
// WithDefaultUnixDevices adds the default devices for unix such as /dev/null, /dev/random to
// the container's resource cgroup spec
func WithDefaultUnixDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setLinux(s)
if s.Linux.Resources == nil {
s.Linux.Resources = &specs.LinuxResources{}
}
intptr := func(i int64) *int64 {
return &i
}
s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, []specs.LinuxDeviceCgroup{
{
// "/dev/null",
Type: "c",
Major: intptr(1),
Minor: intptr(3),
Access: rwm,
Allow: true,
},
{
// "/dev/random",
Type: "c",
Major: intptr(1),
Minor: intptr(8),
Access: rwm,
Allow: true,
},
{
// "/dev/full",
Type: "c",
Major: intptr(1),
Minor: intptr(7),
Access: rwm,
Allow: true,
},
{
// "/dev/tty",
Type: "c",
Major: intptr(5),
Minor: intptr(0),
Access: rwm,
Allow: true,
},
{
// "/dev/zero",
Type: "c",
Major: intptr(1),
Minor: intptr(5),
Access: rwm,
Allow: true,
},
{
// "/dev/urandom",
Type: "c",
Major: intptr(1),
Minor: intptr(9),
Access: rwm,
Allow: true,
},
{
// "/dev/console",
Type: "c",
Major: intptr(5),
Minor: intptr(1),
Access: rwm,
Allow: true,
},
// /dev/pts/ - pts namespaces are "coming soon"
{
Type: "c",
Major: intptr(136),
Access: rwm,
Allow: true,
},
{
Type: "c",
Major: intptr(5),
Minor: intptr(2),
Access: rwm,
Allow: true,
},
{
// tuntap
Type: "c",
Major: intptr(10),
Minor: intptr(200),
Access: rwm,
Allow: true,
},
}...)
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,
)

View File

@ -1,89 +0,0 @@
// +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"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/opencontainers/image-spec/specs-go/v1"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// WithImageConfig configures the spec to from the configuration of an Image
func WithImageConfig(image Image) SpecOpts {
return func(ctx context.Context, client Client, _ *containers.Container, s *Spec) error {
setProcess(s)
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)
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)
}
s.Process.Env = config.Env
s.Process.Args = append(config.Entrypoint, config.Cmd...)
s.Process.User = specs.User{
Username: config.User,
}
return nil
}
}
// WithTTY sets the information on the spec as well as the environment variables for
// using a TTY
func WithTTY(width, height int) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setProcess(s)
s.Process.Terminal = true
if s.Process.ConsoleSize == nil {
s.Process.ConsoleSize = &specs.Box{}
}
s.Process.ConsoleSize.Width = uint(width)
s.Process.ConsoleSize.Height = uint(height)
return nil
}
}
// WithUsername sets the username on the process
func WithUsername(username string) SpecOpts {
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
setProcess(s)
s.Process.User.Username = username
return nil
}
}

View File

@ -1,5 +1,3 @@
// +build !windows
/* /*
Copyright The containerd Authors. Copyright The containerd Authors.
@ -20,6 +18,7 @@ package oci
import ( import (
"context" "context"
"runtime"
"testing" "testing"
"github.com/containerd/containerd/containers" "github.com/containerd/containerd/containers"
@ -39,8 +38,9 @@ func TestGenerateSpec(t *testing.T) {
t.Fatal("GenerateSpec() returns a nil spec") t.Fatal("GenerateSpec() returns a nil spec")
} }
if runtime.GOOS != "windows" {
// check for matching caps // check for matching caps
defaults := defaultCaps() defaults := defaultUnixCaps()
for _, cl := range [][]string{ for _, cl := range [][]string{
s.Process.Capabilities.Bounding, s.Process.Capabilities.Bounding,
s.Process.Capabilities.Permitted, s.Process.Capabilities.Permitted,
@ -55,12 +55,17 @@ func TestGenerateSpec(t *testing.T) {
} }
// check default namespaces // check default namespaces
defaultNS := defaultNamespaces() defaultNS := defaultUnixNamespaces()
for i, ns := range s.Linux.Namespaces { for i, ns := range s.Linux.Namespaces {
if defaultNS[i] != ns { if defaultNS[i] != ns {
t.Errorf("ns at %d does not match set %q != %q", i, defaultNS[i], ns) t.Errorf("ns at %d does not match set %q != %q", i, defaultNS[i], ns)
} }
} }
} else {
if s.Windows == nil {
t.Fatal("Windows section of spec not filled on on Windows platform")
}
}
// test that we don't have tty set // test that we don't have tty set
if s.Process.Terminal { if s.Process.Terminal {
@ -79,10 +84,16 @@ func TestSpecWithTTY(t *testing.T) {
if !s.Process.Terminal { if !s.Process.Terminal {
t.Error("terminal net set WithTTY()") t.Error("terminal net set WithTTY()")
} }
if runtime.GOOS != "windows" {
v := s.Process.Env[len(s.Process.Env)-1] v := s.Process.Env[len(s.Process.Env)-1]
if v != "TERM=xterm" { if v != "TERM=xterm" {
t.Errorf("xterm not set in env for TTY") t.Errorf("xterm not set in env for TTY")
} }
} else {
if len(s.Process.Env) != 0 {
t.Fatal("Windows process args should be empty by default")
}
}
} }
func TestWithLinuxNamespace(t *testing.T) { func TestWithLinuxNamespace(t *testing.T) {
@ -91,12 +102,18 @@ func TestWithLinuxNamespace(t *testing.T) {
ctx := namespaces.WithNamespace(context.Background(), "testing") ctx := namespaces.WithNamespace(context.Background(), "testing")
replacedNS := specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: "/var/run/netns/test"} replacedNS := specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: "/var/run/netns/test"}
s, err := GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, WithLinuxNamespace(replacedNS)) var s *specs.Spec
var err error
if runtime.GOOS != "windows" {
s, err = GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, WithLinuxNamespace(replacedNS))
} else {
s, err = GenerateSpecWithPlatform(ctx, nil, "linux/amd64", &containers.Container{ID: t.Name()}, WithLinuxNamespace(replacedNS))
}
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defaultNS := defaultNamespaces() defaultNS := defaultUnixNamespaces()
found := false found := false
for i, ns := range s.Linux.Namespaces { for i, ns := range s.Linux.Namespaces {
if ns == replacedNS && !found { if ns == replacedNS && !found {
@ -114,9 +131,16 @@ func TestWithCapabilities(t *testing.T) {
ctx := namespaces.WithNamespace(context.Background(), "testing") ctx := namespaces.WithNamespace(context.Background(), "testing")
s, err := GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, opts := []SpecOpts{
WithCapabilities([]string{"CAP_SYS_ADMIN"}), WithCapabilities([]string{"CAP_SYS_ADMIN"}),
) }
var s *specs.Spec
var err error
if runtime.GOOS != "windows" {
s, err = GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, opts...)
} else {
s, err = GenerateSpecWithPlatform(ctx, nil, "linux/amd64", &containers.Container{ID: t.Name()}, opts...)
}
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -166,13 +190,20 @@ func TestWithPrivileged(t *testing.T) {
ctx := namespaces.WithNamespace(context.Background(), "testing") ctx := namespaces.WithNamespace(context.Background(), "testing")
s, err := GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, opts := []SpecOpts{
WithCapabilities(nil), WithCapabilities(nil),
WithMounts([]specs.Mount{ WithMounts([]specs.Mount{
{Type: "cgroup", Destination: "/sys/fs/cgroup", Options: []string{"ro"}}, {Type: "cgroup", Destination: "/sys/fs/cgroup", Options: []string{"ro"}},
}), }),
WithPrivileged, WithPrivileged,
) }
var s *specs.Spec
var err error
if runtime.GOOS != "windows" {
s, err = GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, opts...)
} else {
s, err = GenerateSpecWithPlatform(ctx, nil, "linux/amd64", &containers.Container{ID: t.Name()}, opts...)
}
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,188 +0,0 @@
// +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"
"path/filepath"
"github.com/containerd/containerd/namespaces"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
const (
rwm = "rwm"
defaultRootfsPath = "rootfs"
)
var (
defaultEnv = []string{
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
}
)
func defaultCaps() []string {
return []string{
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETFCAP",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_KILL",
"CAP_AUDIT_WRITE",
}
}
func defaultNamespaces() []specs.LinuxNamespace {
return []specs.LinuxNamespace{
{
Type: specs.PIDNamespace,
},
{
Type: specs.IPCNamespace,
},
{
Type: specs.UTSNamespace,
},
{
Type: specs.MountNamespace,
},
{
Type: specs.NetworkNamespace,
},
}
}
func populateDefaultSpec(ctx context.Context, s *Spec, id string) error {
ns, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return err
}
*s = Spec{
Version: specs.Version,
Root: &specs.Root{
Path: defaultRootfsPath,
},
Process: &specs.Process{
Env: defaultEnv,
Cwd: "/",
NoNewPrivileges: true,
User: specs.User{
UID: 0,
GID: 0,
},
Capabilities: &specs.LinuxCapabilities{
Bounding: defaultCaps(),
Permitted: defaultCaps(),
Inheritable: defaultCaps(),
Effective: defaultCaps(),
},
Rlimits: []specs.POSIXRlimit{
{
Type: "RLIMIT_NOFILE",
Hard: uint64(1024),
Soft: uint64(1024),
},
},
},
Mounts: []specs.Mount{
{
Destination: "/proc",
Type: "proc",
Source: "proc",
},
{
Destination: "/dev",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
{
Destination: "/dev/pts",
Type: "devpts",
Source: "devpts",
Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"},
},
{
Destination: "/dev/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
},
{
Destination: "/dev/mqueue",
Type: "mqueue",
Source: "mqueue",
Options: []string{"nosuid", "noexec", "nodev"},
},
{
Destination: "/sys",
Type: "sysfs",
Source: "sysfs",
Options: []string{"nosuid", "noexec", "nodev", "ro"},
},
{
Destination: "/run",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
},
Linux: &specs.Linux{
MaskedPaths: []string{
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/proc/scsi",
},
ReadonlyPaths: []string{
"/proc/asound",
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger",
},
CgroupsPath: filepath.Join("/", ns, id),
Resources: &specs.LinuxResources{
Devices: []specs.LinuxDeviceCgroup{
{
Allow: false,
Access: rwm,
},
},
},
Namespaces: defaultNamespaces(),
},
}
return nil
}

View File

@ -1,44 +0,0 @@
/*
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"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func populateDefaultSpec(ctx context.Context, s *Spec, id string) error {
*s = Spec{
Version: specs.Version,
Root: &specs.Root{},
Process: &specs.Process{
Cwd: `C:\`,
ConsoleSize: &specs.Box{
Width: 80,
Height: 20,
},
},
Windows: &specs.Windows{
IgnoreFlushesDuringBoot: true,
Network: &specs.WindowsNetwork{
AllowUnqualifiedDNSQuery: true,
},
},
}
return nil
}