123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- //go:build !windows
- package daemon
- import (
- "io/fs"
- "os"
- "strings"
- "testing"
- 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"
- "github.com/imdario/mergo"
- "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))
- })
- }
- }
|