worker: add spec pkg for gdn->oci conversion

As we're now the ones implementing the low-level functionality of
directing containerd to create a container from a very high-level
description that `garden.ContainerSpec` gives, we need a way of coming
up with a valid OCI runtime spec to be used by containerd.

	be.create(garden.ContainerSpec{})
		|
		*--> containerd.newcontaiiner(id, oci_spec)

This commit introduces the `spec` package to deal with such conversion.

The details in this commit are mostly based on the current OCI spec
internal conversion that Guardian (`gdn`) performs (see references),
rather than taking the defaults that `containerd` comes with, as we're
trying to introduce the less amount of friction possible.

ref: https://github.com/opencontainers/runtime-spec/blob/master/spec.md
ref: https://github.com/cloudfoundry/guardian/blob/6b021168907b2/guardiancmd/server.go
ref: https://github.com/cloudfoundry/guardian/blob/0a658a3e51595/guardiancmd/command.go

Signed-off-by: Ciro S. Costa <cscosta@pivotal.io>
This commit is contained in:
Ciro S. Costa 2019-11-18 14:11:29 +00:00
parent 1817c16044
commit dd3037f07c
6 changed files with 678 additions and 0 deletions

View File

@ -0,0 +1,85 @@
package spec
import "github.com/opencontainers/runtime-spec/specs-go"
func OciCapabilities(privileged bool) specs.LinuxCapabilities {
if !privileged {
return UnprivilegedContainerCapabilities
}
return PrivilegedContainerCapabilities
}
var (
PrivilegedContainerCapabilities = specs.LinuxCapabilities{
Effective: privilegedCaps,
Bounding: privilegedCaps,
Inheritable: privilegedCaps,
Permitted: privilegedCaps,
}
UnprivilegedContainerCapabilities = specs.LinuxCapabilities{
Effective: unprivilegedCaps,
Bounding: unprivilegedCaps,
Inheritable: unprivilegedCaps,
Permitted: unprivilegedCaps,
}
unprivilegedCaps = []string{
"CAP_AUDIT_WRITE",
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FOWNER",
"CAP_FSETID",
"CAP_KILL",
"CAP_MKNOD",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SETFCAP",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETUID",
"CAP_SYS_CHROOT",
}
privilegedCaps = []string{
"CAP_AUDIT_CONTROL",
"CAP_AUDIT_READ",
"CAP_AUDIT_WRITE",
"CAP_BLOCK_SUSPEND",
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_DAC_READ_SEARCH",
"CAP_FOWNER",
"CAP_FSETID",
"CAP_IPC_LOCK",
"CAP_IPC_OWNER",
"CAP_KILL",
"CAP_LEASE",
"CAP_LINUX_IMMUTABLE",
"CAP_MAC_ADMIN",
"CAP_MAC_OVERRIDE",
"CAP_MKNOD",
"CAP_NET_ADMIN",
"CAP_NET_BIND_SERVICE",
"CAP_NET_BROADCAST",
"CAP_NET_RAW",
"CAP_SETFCAP",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETUID",
"CAP_SYSLOG",
"CAP_SYS_ADMIN",
"CAP_SYS_BOOT",
"CAP_SYS_CHROOT",
"CAP_SYS_MODULE",
"CAP_SYS_NICE",
"CAP_SYS_PACCT",
"CAP_SYS_PTRACE",
"CAP_SYS_RAWIO",
"CAP_SYS_RESOURCE",
"CAP_SYS_TIME",
"CAP_SYS_TTY_CONFIG",
"CAP_WAKE_ALARM",
}
)

View File

@ -0,0 +1,28 @@
package spec
import "github.com/opencontainers/runtime-spec/specs-go"
var (
AnyContainerDevices = []specs.LinuxDeviceCgroup{
// runc allows these
{Access: "m", Type: "c", Major: deviceWildcard(), Minor: deviceWildcard(), Allow: true},
{Access: "m", Type: "b", Major: deviceWildcard(), Minor: deviceWildcard(), Allow: true},
{Access: "rwm", Type: "c", Major: intRef(1), Minor: intRef(3), Allow: true}, // /dev/null
{Access: "rwm", Type: "c", Major: intRef(1), Minor: intRef(8), Allow: true}, // /dev/random
{Access: "rwm", Type: "c", Major: intRef(1), Minor: intRef(7), Allow: true}, // /dev/full
{Access: "rwm", Type: "c", Major: intRef(5), Minor: intRef(0), Allow: true}, // /dev/tty
{Access: "rwm", Type: "c", Major: intRef(1), Minor: intRef(5), Allow: true}, // /dev/zero
{Access: "rwm", Type: "c", Major: intRef(1), Minor: intRef(9), Allow: true}, // /dev/urandom
{Access: "rwm", Type: "c", Major: intRef(5), Minor: intRef(1), Allow: true}, // /dev/console
{Access: "rwm", Type: "c", Major: intRef(136), Minor: deviceWildcard(), Allow: true}, // /dev/pts/*
{Access: "rwm", Type: "c", Major: intRef(5), Minor: intRef(2), Allow: true}, // /dev/ptmx
{Access: "rwm", Type: "c", Major: intRef(10), Minor: intRef(200), Allow: true}, // /dev/net/tun
// we allow this
{Access: "rwm", Type: "c", Major: intRef(10), Minor: intRef(229), Allow: true}, // /dev/fuse
}
)
func intRef(i int64) *int64 { return &i }
func deviceWildcard() *int64 { return intRef(-1) }

View File

@ -0,0 +1,59 @@
package spec
import "github.com/opencontainers/runtime-spec/specs-go"
var (
InitMount = specs.Mount{
Source: "/usr/local/concourse/bin/init",
Destination: "/tmp/gdn-init",
Type: "bind",
Options: []string{"bind"},
}
AnyContainerMounts = []specs.Mount{
InitMount, // ours
{
Destination: "/proc",
Type: "proc",
Source: "proc",
Options: []string{"nosuid", "noexec", "nodev"},
},
{
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"},
},
}
)

View File

@ -0,0 +1,25 @@
package spec
import "github.com/opencontainers/runtime-spec/specs-go"
var (
PrivilegedContainerNamespaces = []specs.LinuxNamespace{
{Type: specs.PIDNamespace},
{Type: specs.IPCNamespace},
{Type: specs.UTSNamespace},
{Type: specs.MountNamespace},
{Type: specs.NetworkNamespace},
}
UnprivilegedContainerNamespaces = append(PrivilegedContainerNamespaces,
specs.LinuxNamespace{Type: specs.UserNamespace},
)
)
func OciNamespaces(privileged bool) []specs.LinuxNamespace {
if !privileged {
return UnprivilegedContainerNamespaces
}
return PrivilegedContainerNamespaces
}

182
worker/backend/spec/spec.go Normal file
View File

@ -0,0 +1,182 @@
package spec
import (
"fmt"
"path/filepath"
"strings"
"code.cloudfoundry.org/garden"
"github.com/imdario/mergo"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// OciSpec converts a given `garden` container specification to an OCI spec.
//
// TODO
// - limits
// - masked paths
// - rootfs propagation
// - seccomp
// - user namespaces: uid/gid mappings
// x capabilities
// x devices
// x env
// x hostname
// x mounts
// x namespaces
// x rootfs
//
//
func OciSpec(gdn garden.ContainerSpec) (oci *specs.Spec, err error) {
var (
rootfs string
mounts []specs.Mount
)
if gdn.Handle == "" {
err = fmt.Errorf("handle must be specified")
return
}
if gdn.RootFSPath == "" {
gdn.RootFSPath = gdn.Image.URI
}
rootfs, err = rootfsDir(gdn.RootFSPath)
if err != nil {
return
}
mounts, err = OciSpecBindMounts(gdn.BindMounts)
if err != nil {
return
}
oci = merge(defaultGardenOciSpec(gdn.Privileged), &specs.Spec{
Version: specs.Version,
Hostname: gdn.Handle,
Process: &specs.Process{
Env: gdn.Env,
},
Root: &specs.Root{Path: rootfs},
Mounts: mounts,
Annotations: map[string]string(gdn.Properties),
// Linux: &specs.Linux{
// Resources: &specs.LinuxResources{Memory: nil, Cpu: nil},
// },
})
return
}
// OciSpecBindMounts converts garden bindmounts to oci spec mounts.
//
func OciSpecBindMounts(bindMounts []garden.BindMount) (mounts []specs.Mount, err error) {
for _, bindMount := range bindMounts {
if bindMount.SrcPath == "" || bindMount.DstPath == "" {
err = fmt.Errorf("src and dst must not be empty")
return
}
if !filepath.IsAbs(bindMount.SrcPath) || !filepath.IsAbs(bindMount.DstPath) {
err = fmt.Errorf("src and dst must be absolute")
return
}
if bindMount.Origin != garden.BindMountOriginHost {
err = fmt.Errorf("unknown bind mount origin %d", bindMount.Origin)
return
}
mode := "ro"
switch bindMount.Mode {
case garden.BindMountModeRO:
case garden.BindMountModeRW:
mode = "rw"
default:
err = fmt.Errorf("unknown bind mount mode %d", bindMount.Mode)
return
}
mounts = append(mounts, specs.Mount{
Source: bindMount.SrcPath,
Destination: bindMount.DstPath,
Type: "bind",
Options: []string{"bind", mode},
})
}
return
}
// defaultGardenOciSpec repreeseents a default set of properties necessary in
// order to satisfy the garden interface.
//
// ps.: this spec is NOT complet - it must be merged with more properties to
// form a properly working container.
//
func defaultGardenOciSpec(privileged bool) *specs.Spec {
var (
namespaces = OciNamespaces(privileged)
capabilities = OciCapabilities(privileged)
)
return &specs.Spec{
Process: &specs.Process{
Args: []string{"/tmp/gdn-init"},
Capabilities: &capabilities,
Cwd: "/",
},
Linux: &specs.Linux{
Namespaces: namespaces,
Resources: &specs.LinuxResources{
Devices: AnyContainerDevices,
},
},
Mounts: AnyContainerMounts,
}
}
// merge merges an OCI spec `dst` into `src`.
//
func merge(dst, src *specs.Spec) *specs.Spec {
err := mergo.Merge(dst, src, mergo.WithAppendSlice)
if err != nil {
panic(fmt.Errorf(
"failed to merge specs %v %v - programming mistake? %w",
dst, src, err,
))
}
return dst
}
// rootfsDir takes a raw rootfs uri and extracts the directory that it points to,
// if using a valid scheme (`raw://`)
//
func rootfsDir(raw string) (directory string, err error) {
if raw == "" {
err = fmt.Errorf("rootfs must not be empty")
return
}
parts := strings.SplitN(raw, "://", 2)
if len(parts) != 2 {
err = fmt.Errorf("malformatted rootfs: must be of form 'scheme://<abs_dir>'")
return
}
var scheme string
scheme, directory = parts[0], parts[1]
if scheme != "raw" {
err = fmt.Errorf("unsupported scheme '%s'", scheme)
return
}
if !filepath.IsAbs(directory) {
err = fmt.Errorf("directory must be an absolute path")
return
}
return
}

View File

@ -0,0 +1,299 @@
package spec_test
import (
"testing"
"code.cloudfoundry.org/garden"
"github.com/concourse/concourse/worker/backend/spec"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type Suite struct {
suite.Suite
*require.Assertions
}
func (s *Suite) TestContainerSpecValidations() {
for _, tc := range []struct {
desc string
spec garden.ContainerSpec
}{
{
desc: "no handle specified",
spec: garden.ContainerSpec{},
},
{
desc: "rootfsPath not specified",
spec: garden.ContainerSpec{
Handle: "handle",
},
},
{
desc: "rootfsPath without scheme",
spec: garden.ContainerSpec{
Handle: "handle",
RootFSPath: "foo",
},
},
{
desc: "rootfsPath with unknown scheme",
spec: garden.ContainerSpec{
Handle: "handle",
RootFSPath: "weird://foo",
},
},
{
desc: "rootfsPath not being absolute",
spec: garden.ContainerSpec{
Handle: "handle",
RootFSPath: "raw://../not/absolute/at/all",
},
},
{
desc: "both rootfsPath and image specified",
spec: garden.ContainerSpec{
Handle: "handle",
RootFSPath: "foo",
Image: garden.ImageRef{URI: "bar"},
},
},
{
desc: "no rootfsPath, but image specified w/out scheme",
spec: garden.ContainerSpec{
Handle: "handle",
Image: garden.ImageRef{URI: "bar"},
},
},
{
desc: "no rootfsPath, but image specified w/ unknown scheme",
spec: garden.ContainerSpec{
Handle: "handle",
Image: garden.ImageRef{URI: "weird://bar"},
},
},
} {
s.T().Run(tc.desc, func(t *testing.T) {
_, err := spec.OciSpec(tc.spec)
s.Error(err)
})
}
}
func (s *Suite) TestOciSpecBindMounts() {
for _, tc := range []struct {
desc string
mounts []garden.BindMount
expected []specs.Mount
succeeds bool
}{
{
desc: "unknown mode",
succeeds: false,
mounts: []garden.BindMount{
{
SrcPath: "/a",
DstPath: "/b",
Mode: 123,
Origin: garden.BindMountOriginHost,
},
},
},
{
desc: "unknown origin",
succeeds: false,
mounts: []garden.BindMount{
{
SrcPath: "/a",
DstPath: "/b",
Mode: garden.BindMountModeRO,
Origin: 123,
},
},
},
{
desc: "w/out src",
succeeds: false,
mounts: []garden.BindMount{
{
DstPath: "/b",
Mode: garden.BindMountModeRO,
Origin: garden.BindMountOriginHost,
},
},
},
{
desc: "non-absolute src",
succeeds: false,
mounts: []garden.BindMount{
{
DstPath: "/b",
Mode: garden.BindMountModeRO,
Origin: garden.BindMountOriginHost,
},
},
},
{
desc: "w/out dest",
succeeds: false,
mounts: []garden.BindMount{
{
SrcPath: "/a",
Mode: garden.BindMountModeRO,
Origin: garden.BindMountOriginHost,
},
},
},
{
desc: "non-absolute dest",
succeeds: false,
mounts: []garden.BindMount{
{
DstPath: "/b",
Mode: garden.BindMountModeRO,
Origin: garden.BindMountOriginHost,
},
},
},
} {
s.T().Run(tc.desc, func(t *testing.T) {
actual, err := spec.OciSpecBindMounts(tc.mounts)
if !tc.succeeds {
s.Error(err)
return
}
s.NoError(err)
s.Equal(tc.expected, actual)
})
}
}
func (s *Suite) TestOciNamespaces() {
for _, tc := range []struct {
desc string
privileged bool
expected []specs.LinuxNamespace
}{
{
desc: "privileged",
privileged: true,
expected: spec.PrivilegedContainerNamespaces,
},
{
desc: "unprivileged",
privileged: false,
expected: spec.UnprivilegedContainerNamespaces,
},
} {
s.T().Run(tc.desc, func(t *testing.T) {
s.Equal(tc.expected, spec.OciNamespaces(tc.privileged))
})
}
}
func (s *Suite) TestOciCapabilities() {
for _, tc := range []struct {
desc string
privileged bool
expected specs.LinuxCapabilities
}{
{
desc: "privileged",
privileged: true,
expected: spec.PrivilegedContainerCapabilities,
},
{
desc: "unprivileged",
privileged: false,
expected: spec.UnprivilegedContainerCapabilities,
},
} {
s.T().Run(tc.desc, func(t *testing.T) {
s.Equal(tc.expected, spec.OciCapabilities(tc.privileged))
})
}
}
func (s *Suite) TestContainerSpec() {
var minimalContainerSpec = garden.ContainerSpec{
Handle: "handle", RootFSPath: "raw:///rootfs",
}
for _, tc := range []struct {
desc string
gdn garden.ContainerSpec
check func(*specs.Spec)
}{
{
desc: "defaults",
gdn: minimalContainerSpec,
check: func(oci *specs.Spec) {
s.Equal("/", oci.Process.Cwd)
s.Equal([]string{"/tmp/gdn-init"}, oci.Process.Args)
s.Equal(oci.Mounts, spec.AnyContainerMounts)
s.Equal(minimalContainerSpec.Handle, oci.Hostname)
s.Equal(spec.AnyContainerDevices, oci.Linux.Resources.Devices)
},
},
{
desc: "env",
gdn: garden.ContainerSpec{
Handle: "handle", RootFSPath: "raw:///rootfs",
Env: []string{"foo=bar"},
},
check: func(oci *specs.Spec) {
s.Equal([]string{"foo=bar"}, oci.Process.Env)
},
},
{
desc: "mounts",
gdn: garden.ContainerSpec{
Handle: "handle", RootFSPath: "raw:///rootfs",
BindMounts: []garden.BindMount{
{ // ro mount
SrcPath: "/a",
DstPath: "/b",
Mode: garden.BindMountModeRO,
Origin: garden.BindMountOriginHost,
},
{ // rw mount
SrcPath: "/a",
DstPath: "/b",
Mode: garden.BindMountModeRW,
Origin: garden.BindMountOriginHost,
},
},
},
check: func(oci *specs.Spec) {
s.Contains(oci.Mounts, specs.Mount{
Source: "/a",
Destination: "/b",
Type: "bind",
Options: []string{"bind", "ro"},
})
s.Contains(oci.Mounts, specs.Mount{
Source: "/a",
Destination: "/b",
Type: "bind",
Options: []string{"bind", "rw"},
})
},
},
} {
s.T().Run(tc.desc, func(t *testing.T) {
actual, err := spec.OciSpec(tc.gdn)
s.NoError(err)
tc.check(actual)
})
}
}
func TestSuite(t *testing.T) {
suite.Run(t, &Suite{
Assertions: require.New(t),
})
}