blob: eb4bd3ac1db80160f5e70cd387a6d3adb1a9332a [file] [log] [blame]
//go:build !windows
package daemon
import (
"io/fs"
"os"
"strings"
"testing"
"dario.cat/mergo"
runtimeoptions_v1 "github.com/containerd/containerd/pkg/runtimeoptions/v1"
"github.com/containerd/containerd/plugin"
v2runcoptions "github.com/containerd/containerd/runtime/v2/runc/options"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/daemon/config"
"github.com/docker/docker/errdefs"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/proto"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestSetupRuntimes(t *testing.T) {
cases := []struct {
name string
config *config.Config
expectErr string
}{
{
name: "Empty",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {},
},
},
expectErr: "either a runtimeType or a path must be configured",
},
{
name: "ArgsOnly",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {Args: []string{"foo", "bar"}},
},
},
expectErr: "either a runtimeType or a path must be configured",
},
{
name: "OptionsOnly",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {Options: map[string]interface{}{"hello": "world"}},
},
},
expectErr: "either a runtimeType or a path must be configured",
},
{
name: "PathAndType",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {Path: "/bin/true", Type: "io.containerd.runsc.v1"},
},
},
expectErr: "cannot configure both",
},
{
name: "PathAndOptions",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {Path: "/bin/true", Options: map[string]interface{}{"a": "b"}},
},
},
expectErr: "options cannot be used with a path runtime",
},
{
name: "TypeAndArgs",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {Type: "io.containerd.runsc.v1", Args: []string{"--version"}},
},
},
expectErr: "args cannot be used with a runtimeType runtime",
},
{
name: "PathArgsOptions",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {
Path: "/bin/true",
Args: []string{"--version"},
Options: map[string]interface{}{"hmm": 3},
},
},
},
expectErr: "options cannot be used with a path runtime",
},
{
name: "TypeOptionsArgs",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {
Type: "io.containerd.kata.v2",
Options: map[string]interface{}{"a": "b"},
Args: []string{"--help"},
},
},
},
expectErr: "args cannot be used with a runtimeType runtime",
},
{
name: "PathArgsTypeOptions",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"myruntime": {
Path: "/bin/true",
Args: []string{"foo"},
Type: "io.containerd.runsc.v1",
Options: map[string]interface{}{"a": "b"},
},
},
},
expectErr: "cannot configure both",
},
{
name: "CannotOverrideStockRuntime",
config: &config.Config{
Runtimes: map[string]system.Runtime{
config.StockRuntimeName: {},
},
},
expectErr: `runtime name 'runc' is reserved`,
},
{
name: "SetStockRuntimeAsDefault",
config: &config.Config{
CommonConfig: config.CommonConfig{
DefaultRuntime: config.StockRuntimeName,
},
},
},
{
name: "SetLinuxRuntimeAsDefault",
config: &config.Config{
CommonConfig: config.CommonConfig{
DefaultRuntime: linuxV2RuntimeName,
},
},
},
{
name: "CannotSetBogusRuntimeAsDefault",
config: &config.Config{
CommonConfig: config.CommonConfig{
DefaultRuntime: "notdefined",
},
},
expectErr: "specified default runtime 'notdefined' does not exist",
},
{
name: "SetDefinedRuntimeAsDefault",
config: &config.Config{
Runtimes: map[string]system.Runtime{
"some-runtime": {
Path: "/usr/local/bin/file-not-found",
},
},
CommonConfig: config.CommonConfig{
DefaultRuntime: "some-runtime",
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cfg, err := config.New()
assert.NilError(t, err)
cfg.Root = t.TempDir()
assert.NilError(t, mergo.Merge(cfg, tc.config, mergo.WithOverride))
assert.Assert(t, initRuntimesDir(cfg))
_, err = setupRuntimes(cfg)
if tc.expectErr == "" {
assert.NilError(t, err)
} else {
assert.ErrorContains(t, err, tc.expectErr)
}
})
}
}
func TestGetRuntime(t *testing.T) {
// Configured runtimes can have any arbitrary name, including names
// which would not be allowed as implicit runtime names. Explicit takes
// precedence over implicit.
const configuredRtName = "my/custom.runtime.v1"
configuredRuntime := system.Runtime{Path: "/bin/true"}
const rtWithArgsName = "withargs"
rtWithArgs := system.Runtime{
Path: "/bin/false",
Args: []string{"--version"},
}
const shimWithOptsName = "shimwithopts"
shimWithOpts := system.Runtime{
Type: plugin.RuntimeRuncV2,
Options: map[string]interface{}{"IoUid": 42},
}
const shimAliasName = "wasmedge"
shimAlias := system.Runtime{Type: "io.containerd.wasmedge.v1"}
const configuredShimByPathName = "shimwithpath"
configuredShimByPath := system.Runtime{Type: "/path/to/my/shim"}
// A runtime configured with the generic 'runtimeoptions/v1.Options' shim configuration options.
// https://gvisor.dev/docs/user_guide/containerd/configuration/#:~:text=to%20the%20shim.-,Containerd%201.3%2B,-Starting%20in%201.3
const gvisorName = "gvisor"
gvisorRuntime := system.Runtime{
Type: "io.containerd.runsc.v1",
Options: map[string]interface{}{
"TypeUrl": "io.containerd.runsc.v1.options",
"ConfigPath": "/path/to/runsc.toml",
},
}
cfg, err := config.New()
assert.NilError(t, err)
cfg.Root = t.TempDir()
cfg.Runtimes = map[string]system.Runtime{
configuredRtName: configuredRuntime,
rtWithArgsName: rtWithArgs,
shimWithOptsName: shimWithOpts,
shimAliasName: shimAlias,
configuredShimByPathName: configuredShimByPath,
gvisorName: gvisorRuntime,
}
assert.NilError(t, initRuntimesDir(cfg))
runtimes, err := setupRuntimes(cfg)
assert.NilError(t, err)
stockRuntime, ok := runtimes.configured[config.StockRuntimeName]
assert.Assert(t, ok, "stock runtime could not be found (test needs to be updated)")
stockRuntime.Features = nil
configdOpts := proto.Clone(stockRuntime.Opts.(*v2runcoptions.Options)).(*v2runcoptions.Options)
configdOpts.BinaryName = configuredRuntime.Path
wantConfigdRuntime := &shimConfig{
Shim: stockRuntime.Shim,
Opts: configdOpts,
}
for _, tt := range []struct {
name, runtime string
want *shimConfig
}{
{
name: "StockRuntime",
runtime: config.StockRuntimeName,
want: stockRuntime,
},
{
name: "ShimName",
runtime: "io.containerd.my-shim.v42",
want: &shimConfig{Shim: "io.containerd.my-shim.v42"},
},
{
// containerd is pretty loose about the format of runtime names. Perhaps too
// loose. The only requirements are that the name contain a dot and (depending
// on the containerd version) not start with a dot. It does not enforce any
// particular format of the dot-delimited components of the name.
name: "VersionlessShimName",
runtime: "io.containerd.my-shim",
want: &shimConfig{Shim: "io.containerd.my-shim"},
},
{
name: "IllformedShimName",
runtime: "myshim",
},
{
name: "EmptyString",
runtime: "",
want: stockRuntime,
},
{
name: "PathToShim",
runtime: "/path/to/runc",
},
{
name: "PathToShimName",
runtime: "/path/to/io.containerd.runc.v2",
},
{
name: "RelPathToShim",
runtime: "my/io.containerd.runc.v2",
},
{
name: "ConfiguredRuntime",
runtime: configuredRtName,
want: wantConfigdRuntime,
},
{
name: "ShimWithOpts",
runtime: shimWithOptsName,
want: &shimConfig{
Shim: shimWithOpts.Type,
Opts: &v2runcoptions.Options{IoUid: 42},
},
},
{
name: "ShimAlias",
runtime: shimAliasName,
want: &shimConfig{Shim: shimAlias.Type},
},
{
name: "ConfiguredShimByPath",
runtime: configuredShimByPathName,
want: &shimConfig{Shim: configuredShimByPath.Type},
},
{
name: "ConfiguredShimWithRuntimeoptionsShimConfig",
runtime: gvisorName,
want: &shimConfig{
Shim: gvisorRuntime.Type,
Opts: &runtimeoptions_v1.Options{
TypeUrl: gvisorRuntime.Options["TypeUrl"].(string),
ConfigPath: gvisorRuntime.Options["ConfigPath"].(string),
},
},
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
shim, opts, err := runtimes.Get(tt.runtime)
if tt.want != nil {
assert.Check(t, err)
got := &shimConfig{Shim: shim, Opts: opts}
assert.Check(t, is.DeepEqual(got, tt.want,
cmpopts.IgnoreUnexported(runtimeoptions_v1.Options{}),
cmpopts.IgnoreUnexported(v2runcoptions.Options{}),
))
} else {
assert.Check(t, is.Equal(shim, ""))
assert.Check(t, is.Nil(opts))
assert.Check(t, errdefs.IsInvalidParameter(err), "[%T] %[1]v", err)
}
})
}
t.Run("RuntimeWithArgs", func(t *testing.T) {
shim, opts, err := runtimes.Get(rtWithArgsName)
assert.Check(t, err)
assert.Check(t, is.Equal(shim, stockRuntime.Shim))
runcopts, ok := opts.(*v2runcoptions.Options)
if assert.Check(t, ok, "runtimes.Get() opts = type %T, want *v2runcoptions.Options", opts) {
wrapper, err := os.ReadFile(runcopts.BinaryName)
if assert.Check(t, err) {
assert.Check(t, is.Contains(string(wrapper),
strings.Join(append([]string{rtWithArgs.Path}, rtWithArgs.Args...), " ")))
}
}
})
}
func TestGetRuntime_PreflightCheck(t *testing.T) {
cfg, err := config.New()
assert.NilError(t, err)
cfg.Root = t.TempDir()
cfg.Runtimes = map[string]system.Runtime{
"path-only": {
Path: "/usr/local/bin/file-not-found",
},
"with-args": {
Path: "/usr/local/bin/file-not-found",
Args: []string{"--arg"},
},
}
assert.NilError(t, initRuntimesDir(cfg))
runtimes, err := setupRuntimes(cfg)
assert.NilError(t, err, "runtime paths should not be validated during setupRuntimes()")
t.Run("PathOnly", func(t *testing.T) {
_, _, err := runtimes.Get("path-only")
assert.NilError(t, err, "custom runtimes without wrapper scripts should not have pre-flight checks")
})
t.Run("WithArgs", func(t *testing.T) {
_, _, err := runtimes.Get("with-args")
assert.ErrorIs(t, err, fs.ErrNotExist)
})
}
// TestRuntimeWrapping checks that reloading runtime config does not delete or
// modify existing wrapper scripts, which could break lifecycle management of
// existing containers.
func TestRuntimeWrapping(t *testing.T) {
cfg, err := config.New()
assert.NilError(t, err)
cfg.Root = t.TempDir()
cfg.Runtimes = map[string]system.Runtime{
"change-args": {
Path: "/bin/true",
Args: []string{"foo", "bar"},
},
"dupe": {
Path: "/bin/true",
Args: []string{"foo", "bar"},
},
"change-path": {
Path: "/bin/true",
Args: []string{"baz"},
},
"drop-args": {
Path: "/bin/true",
Args: []string{"some", "arguments"},
},
"goes-away": {
Path: "/bin/true",
Args: []string{"bye"},
},
}
assert.NilError(t, initRuntimesDir(cfg))
rt, err := setupRuntimes(cfg)
assert.Check(t, err)
type WrapperInfo struct{ BinaryName, Content string }
wrappers := make(map[string]WrapperInfo)
for name := range cfg.Runtimes {
_, opts, err := rt.Get(name)
if assert.Check(t, err, "rt.Get(%q)", name) {
binary := opts.(*v2runcoptions.Options).BinaryName
content, err := os.ReadFile(binary)
assert.Check(t, err, "could not read wrapper script contents for runtime %q", binary)
wrappers[name] = WrapperInfo{BinaryName: binary, Content: string(content)}
}
}
cfg.Runtimes["change-args"] = system.Runtime{
Path: cfg.Runtimes["change-args"].Path,
Args: []string{"baz", "quux"},
}
cfg.Runtimes["change-path"] = system.Runtime{
Path: "/bin/false",
Args: cfg.Runtimes["change-path"].Args,
}
cfg.Runtimes["drop-args"] = system.Runtime{
Path: cfg.Runtimes["drop-args"].Path,
}
delete(cfg.Runtimes, "goes-away")
_, err = setupRuntimes(cfg)
assert.Check(t, err)
for name, info := range wrappers {
t.Run(name, func(t *testing.T) {
content, err := os.ReadFile(info.BinaryName)
assert.NilError(t, err)
assert.DeepEqual(t, info.Content, string(content))
})
}
}