| //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)) |
| }) |
| } |
| } |