github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/daemon/runtime_unix_test.go (about)

     1  //go:build !windows
     2  
     3  package daemon
     4  
     5  import (
     6  	"io/fs"
     7  	"os"
     8  	"strings"
     9  	"testing"
    10  
    11  	"dario.cat/mergo"
    12  	runtimeoptions_v1 "github.com/containerd/containerd/pkg/runtimeoptions/v1"
    13  	"github.com/containerd/containerd/plugin"
    14  	v2runcoptions "github.com/containerd/containerd/runtime/v2/runc/options"
    15  	"github.com/docker/docker/api/types/system"
    16  	"github.com/docker/docker/daemon/config"
    17  	"github.com/docker/docker/errdefs"
    18  	"github.com/google/go-cmp/cmp/cmpopts"
    19  	"google.golang.org/protobuf/proto"
    20  	"gotest.tools/v3/assert"
    21  	is "gotest.tools/v3/assert/cmp"
    22  )
    23  
    24  func TestSetupRuntimes(t *testing.T) {
    25  	cases := []struct {
    26  		name      string
    27  		config    *config.Config
    28  		expectErr string
    29  	}{
    30  		{
    31  			name: "Empty",
    32  			config: &config.Config{
    33  				Runtimes: map[string]system.Runtime{
    34  					"myruntime": {},
    35  				},
    36  			},
    37  			expectErr: "either a runtimeType or a path must be configured",
    38  		},
    39  		{
    40  			name: "ArgsOnly",
    41  			config: &config.Config{
    42  				Runtimes: map[string]system.Runtime{
    43  					"myruntime": {Args: []string{"foo", "bar"}},
    44  				},
    45  			},
    46  			expectErr: "either a runtimeType or a path must be configured",
    47  		},
    48  		{
    49  			name: "OptionsOnly",
    50  			config: &config.Config{
    51  				Runtimes: map[string]system.Runtime{
    52  					"myruntime": {Options: map[string]interface{}{"hello": "world"}},
    53  				},
    54  			},
    55  			expectErr: "either a runtimeType or a path must be configured",
    56  		},
    57  		{
    58  			name: "PathAndType",
    59  			config: &config.Config{
    60  				Runtimes: map[string]system.Runtime{
    61  					"myruntime": {Path: "/bin/true", Type: "io.containerd.runsc.v1"},
    62  				},
    63  			},
    64  			expectErr: "cannot configure both",
    65  		},
    66  		{
    67  			name: "PathAndOptions",
    68  			config: &config.Config{
    69  				Runtimes: map[string]system.Runtime{
    70  					"myruntime": {Path: "/bin/true", Options: map[string]interface{}{"a": "b"}},
    71  				},
    72  			},
    73  			expectErr: "options cannot be used with a path runtime",
    74  		},
    75  		{
    76  			name: "TypeAndArgs",
    77  			config: &config.Config{
    78  				Runtimes: map[string]system.Runtime{
    79  					"myruntime": {Type: "io.containerd.runsc.v1", Args: []string{"--version"}},
    80  				},
    81  			},
    82  			expectErr: "args cannot be used with a runtimeType runtime",
    83  		},
    84  		{
    85  			name: "PathArgsOptions",
    86  			config: &config.Config{
    87  				Runtimes: map[string]system.Runtime{
    88  					"myruntime": {
    89  						Path:    "/bin/true",
    90  						Args:    []string{"--version"},
    91  						Options: map[string]interface{}{"hmm": 3},
    92  					},
    93  				},
    94  			},
    95  			expectErr: "options cannot be used with a path runtime",
    96  		},
    97  		{
    98  			name: "TypeOptionsArgs",
    99  			config: &config.Config{
   100  				Runtimes: map[string]system.Runtime{
   101  					"myruntime": {
   102  						Type:    "io.containerd.kata.v2",
   103  						Options: map[string]interface{}{"a": "b"},
   104  						Args:    []string{"--help"},
   105  					},
   106  				},
   107  			},
   108  			expectErr: "args cannot be used with a runtimeType runtime",
   109  		},
   110  		{
   111  			name: "PathArgsTypeOptions",
   112  			config: &config.Config{
   113  				Runtimes: map[string]system.Runtime{
   114  					"myruntime": {
   115  						Path:    "/bin/true",
   116  						Args:    []string{"foo"},
   117  						Type:    "io.containerd.runsc.v1",
   118  						Options: map[string]interface{}{"a": "b"},
   119  					},
   120  				},
   121  			},
   122  			expectErr: "cannot configure both",
   123  		},
   124  		{
   125  			name: "CannotOverrideStockRuntime",
   126  			config: &config.Config{
   127  				Runtimes: map[string]system.Runtime{
   128  					config.StockRuntimeName: {},
   129  				},
   130  			},
   131  			expectErr: `runtime name 'runc' is reserved`,
   132  		},
   133  		{
   134  			name: "SetStockRuntimeAsDefault",
   135  			config: &config.Config{
   136  				CommonConfig: config.CommonConfig{
   137  					DefaultRuntime: config.StockRuntimeName,
   138  				},
   139  			},
   140  		},
   141  		{
   142  			name: "SetLinuxRuntimeAsDefault",
   143  			config: &config.Config{
   144  				CommonConfig: config.CommonConfig{
   145  					DefaultRuntime: linuxV2RuntimeName,
   146  				},
   147  			},
   148  		},
   149  		{
   150  			name: "CannotSetBogusRuntimeAsDefault",
   151  			config: &config.Config{
   152  				CommonConfig: config.CommonConfig{
   153  					DefaultRuntime: "notdefined",
   154  				},
   155  			},
   156  			expectErr: "specified default runtime 'notdefined' does not exist",
   157  		},
   158  		{
   159  			name: "SetDefinedRuntimeAsDefault",
   160  			config: &config.Config{
   161  				Runtimes: map[string]system.Runtime{
   162  					"some-runtime": {
   163  						Path: "/usr/local/bin/file-not-found",
   164  					},
   165  				},
   166  				CommonConfig: config.CommonConfig{
   167  					DefaultRuntime: "some-runtime",
   168  				},
   169  			},
   170  		},
   171  	}
   172  	for _, tc := range cases {
   173  		tc := tc
   174  		t.Run(tc.name, func(t *testing.T) {
   175  			cfg, err := config.New()
   176  			assert.NilError(t, err)
   177  			cfg.Root = t.TempDir()
   178  			assert.NilError(t, mergo.Merge(cfg, tc.config, mergo.WithOverride))
   179  			assert.Assert(t, initRuntimesDir(cfg))
   180  
   181  			_, err = setupRuntimes(cfg)
   182  			if tc.expectErr == "" {
   183  				assert.NilError(t, err)
   184  			} else {
   185  				assert.ErrorContains(t, err, tc.expectErr)
   186  			}
   187  		})
   188  	}
   189  }
   190  
   191  func TestGetRuntime(t *testing.T) {
   192  	// Configured runtimes can have any arbitrary name, including names
   193  	// which would not be allowed as implicit runtime names. Explicit takes
   194  	// precedence over implicit.
   195  	const configuredRtName = "my/custom.runtime.v1"
   196  	configuredRuntime := system.Runtime{Path: "/bin/true"}
   197  
   198  	const rtWithArgsName = "withargs"
   199  	rtWithArgs := system.Runtime{
   200  		Path: "/bin/false",
   201  		Args: []string{"--version"},
   202  	}
   203  
   204  	const shimWithOptsName = "shimwithopts"
   205  	shimWithOpts := system.Runtime{
   206  		Type:    plugin.RuntimeRuncV2,
   207  		Options: map[string]interface{}{"IoUid": 42},
   208  	}
   209  
   210  	const shimAliasName = "wasmedge"
   211  	shimAlias := system.Runtime{Type: "io.containerd.wasmedge.v1"}
   212  
   213  	const configuredShimByPathName = "shimwithpath"
   214  	configuredShimByPath := system.Runtime{Type: "/path/to/my/shim"}
   215  
   216  	// A runtime configured with the generic 'runtimeoptions/v1.Options' shim configuration options.
   217  	// https://gvisor.dev/docs/user_guide/containerd/configuration/#:~:text=to%20the%20shim.-,Containerd%201.3%2B,-Starting%20in%201.3
   218  	const gvisorName = "gvisor"
   219  	gvisorRuntime := system.Runtime{
   220  		Type: "io.containerd.runsc.v1",
   221  		Options: map[string]interface{}{
   222  			"TypeUrl":    "io.containerd.runsc.v1.options",
   223  			"ConfigPath": "/path/to/runsc.toml",
   224  		},
   225  	}
   226  
   227  	cfg, err := config.New()
   228  	assert.NilError(t, err)
   229  
   230  	cfg.Root = t.TempDir()
   231  	cfg.Runtimes = map[string]system.Runtime{
   232  		configuredRtName:         configuredRuntime,
   233  		rtWithArgsName:           rtWithArgs,
   234  		shimWithOptsName:         shimWithOpts,
   235  		shimAliasName:            shimAlias,
   236  		configuredShimByPathName: configuredShimByPath,
   237  		gvisorName:               gvisorRuntime,
   238  	}
   239  	assert.NilError(t, initRuntimesDir(cfg))
   240  	runtimes, err := setupRuntimes(cfg)
   241  	assert.NilError(t, err)
   242  
   243  	stockRuntime, ok := runtimes.configured[config.StockRuntimeName]
   244  	assert.Assert(t, ok, "stock runtime could not be found (test needs to be updated)")
   245  	stockRuntime.Features = nil
   246  
   247  	configdOpts := proto.Clone(stockRuntime.Opts.(*v2runcoptions.Options)).(*v2runcoptions.Options)
   248  	configdOpts.BinaryName = configuredRuntime.Path
   249  	wantConfigdRuntime := &shimConfig{
   250  		Shim: stockRuntime.Shim,
   251  		Opts: configdOpts,
   252  	}
   253  
   254  	for _, tt := range []struct {
   255  		name, runtime string
   256  		want          *shimConfig
   257  	}{
   258  		{
   259  			name:    "StockRuntime",
   260  			runtime: config.StockRuntimeName,
   261  			want:    stockRuntime,
   262  		},
   263  		{
   264  			name:    "ShimName",
   265  			runtime: "io.containerd.my-shim.v42",
   266  			want:    &shimConfig{Shim: "io.containerd.my-shim.v42"},
   267  		},
   268  		{
   269  			// containerd is pretty loose about the format of runtime names. Perhaps too
   270  			// loose. The only requirements are that the name contain a dot and (depending
   271  			// on the containerd version) not start with a dot. It does not enforce any
   272  			// particular format of the dot-delimited components of the name.
   273  			name:    "VersionlessShimName",
   274  			runtime: "io.containerd.my-shim",
   275  			want:    &shimConfig{Shim: "io.containerd.my-shim"},
   276  		},
   277  		{
   278  			name:    "IllformedShimName",
   279  			runtime: "myshim",
   280  		},
   281  		{
   282  			name:    "EmptyString",
   283  			runtime: "",
   284  			want:    stockRuntime,
   285  		},
   286  		{
   287  			name:    "PathToShim",
   288  			runtime: "/path/to/runc",
   289  		},
   290  		{
   291  			name:    "PathToShimName",
   292  			runtime: "/path/to/io.containerd.runc.v2",
   293  		},
   294  		{
   295  			name:    "RelPathToShim",
   296  			runtime: "my/io.containerd.runc.v2",
   297  		},
   298  		{
   299  			name:    "ConfiguredRuntime",
   300  			runtime: configuredRtName,
   301  			want:    wantConfigdRuntime,
   302  		},
   303  		{
   304  			name:    "ShimWithOpts",
   305  			runtime: shimWithOptsName,
   306  			want: &shimConfig{
   307  				Shim: shimWithOpts.Type,
   308  				Opts: &v2runcoptions.Options{IoUid: 42},
   309  			},
   310  		},
   311  		{
   312  			name:    "ShimAlias",
   313  			runtime: shimAliasName,
   314  			want:    &shimConfig{Shim: shimAlias.Type},
   315  		},
   316  		{
   317  			name:    "ConfiguredShimByPath",
   318  			runtime: configuredShimByPathName,
   319  			want:    &shimConfig{Shim: configuredShimByPath.Type},
   320  		},
   321  		{
   322  			name:    "ConfiguredShimWithRuntimeoptionsShimConfig",
   323  			runtime: gvisorName,
   324  			want: &shimConfig{
   325  				Shim: gvisorRuntime.Type,
   326  				Opts: &runtimeoptions_v1.Options{
   327  					TypeUrl:    gvisorRuntime.Options["TypeUrl"].(string),
   328  					ConfigPath: gvisorRuntime.Options["ConfigPath"].(string),
   329  				},
   330  			},
   331  		},
   332  	} {
   333  		tt := tt
   334  		t.Run(tt.name, func(t *testing.T) {
   335  			shim, opts, err := runtimes.Get(tt.runtime)
   336  			if tt.want != nil {
   337  				assert.Check(t, err)
   338  				got := &shimConfig{Shim: shim, Opts: opts}
   339  				assert.Check(t, is.DeepEqual(got, tt.want,
   340  					cmpopts.IgnoreUnexported(runtimeoptions_v1.Options{}),
   341  					cmpopts.IgnoreUnexported(v2runcoptions.Options{}),
   342  				))
   343  			} else {
   344  				assert.Check(t, is.Equal(shim, ""))
   345  				assert.Check(t, is.Nil(opts))
   346  				assert.Check(t, errdefs.IsInvalidParameter(err), "[%T] %[1]v", err)
   347  			}
   348  		})
   349  	}
   350  	t.Run("RuntimeWithArgs", func(t *testing.T) {
   351  		shim, opts, err := runtimes.Get(rtWithArgsName)
   352  		assert.Check(t, err)
   353  		assert.Check(t, is.Equal(shim, stockRuntime.Shim))
   354  		runcopts, ok := opts.(*v2runcoptions.Options)
   355  		if assert.Check(t, ok, "runtimes.Get() opts = type %T, want *v2runcoptions.Options", opts) {
   356  			wrapper, err := os.ReadFile(runcopts.BinaryName)
   357  			if assert.Check(t, err) {
   358  				assert.Check(t, is.Contains(string(wrapper),
   359  					strings.Join(append([]string{rtWithArgs.Path}, rtWithArgs.Args...), " ")))
   360  			}
   361  		}
   362  	})
   363  }
   364  
   365  func TestGetRuntime_PreflightCheck(t *testing.T) {
   366  	cfg, err := config.New()
   367  	assert.NilError(t, err)
   368  
   369  	cfg.Root = t.TempDir()
   370  	cfg.Runtimes = map[string]system.Runtime{
   371  		"path-only": {
   372  			Path: "/usr/local/bin/file-not-found",
   373  		},
   374  		"with-args": {
   375  			Path: "/usr/local/bin/file-not-found",
   376  			Args: []string{"--arg"},
   377  		},
   378  	}
   379  	assert.NilError(t, initRuntimesDir(cfg))
   380  	runtimes, err := setupRuntimes(cfg)
   381  	assert.NilError(t, err, "runtime paths should not be validated during setupRuntimes()")
   382  
   383  	t.Run("PathOnly", func(t *testing.T) {
   384  		_, _, err := runtimes.Get("path-only")
   385  		assert.NilError(t, err, "custom runtimes without wrapper scripts should not have pre-flight checks")
   386  	})
   387  	t.Run("WithArgs", func(t *testing.T) {
   388  		_, _, err := runtimes.Get("with-args")
   389  		assert.ErrorIs(t, err, fs.ErrNotExist)
   390  	})
   391  }
   392  
   393  // TestRuntimeWrapping checks that reloading runtime config does not delete or
   394  // modify existing wrapper scripts, which could break lifecycle management of
   395  // existing containers.
   396  func TestRuntimeWrapping(t *testing.T) {
   397  	cfg, err := config.New()
   398  	assert.NilError(t, err)
   399  	cfg.Root = t.TempDir()
   400  	cfg.Runtimes = map[string]system.Runtime{
   401  		"change-args": {
   402  			Path: "/bin/true",
   403  			Args: []string{"foo", "bar"},
   404  		},
   405  		"dupe": {
   406  			Path: "/bin/true",
   407  			Args: []string{"foo", "bar"},
   408  		},
   409  		"change-path": {
   410  			Path: "/bin/true",
   411  			Args: []string{"baz"},
   412  		},
   413  		"drop-args": {
   414  			Path: "/bin/true",
   415  			Args: []string{"some", "arguments"},
   416  		},
   417  		"goes-away": {
   418  			Path: "/bin/true",
   419  			Args: []string{"bye"},
   420  		},
   421  	}
   422  	assert.NilError(t, initRuntimesDir(cfg))
   423  	rt, err := setupRuntimes(cfg)
   424  	assert.Check(t, err)
   425  
   426  	type WrapperInfo struct{ BinaryName, Content string }
   427  	wrappers := make(map[string]WrapperInfo)
   428  	for name := range cfg.Runtimes {
   429  		_, opts, err := rt.Get(name)
   430  		if assert.Check(t, err, "rt.Get(%q)", name) {
   431  			binary := opts.(*v2runcoptions.Options).BinaryName
   432  			content, err := os.ReadFile(binary)
   433  			assert.Check(t, err, "could not read wrapper script contents for runtime %q", binary)
   434  			wrappers[name] = WrapperInfo{BinaryName: binary, Content: string(content)}
   435  		}
   436  	}
   437  
   438  	cfg.Runtimes["change-args"] = system.Runtime{
   439  		Path: cfg.Runtimes["change-args"].Path,
   440  		Args: []string{"baz", "quux"},
   441  	}
   442  	cfg.Runtimes["change-path"] = system.Runtime{
   443  		Path: "/bin/false",
   444  		Args: cfg.Runtimes["change-path"].Args,
   445  	}
   446  	cfg.Runtimes["drop-args"] = system.Runtime{
   447  		Path: cfg.Runtimes["drop-args"].Path,
   448  	}
   449  	delete(cfg.Runtimes, "goes-away")
   450  
   451  	_, err = setupRuntimes(cfg)
   452  	assert.Check(t, err)
   453  
   454  	for name, info := range wrappers {
   455  		t.Run(name, func(t *testing.T) {
   456  			content, err := os.ReadFile(info.BinaryName)
   457  			assert.NilError(t, err)
   458  			assert.DeepEqual(t, info.Content, string(content))
   459  		})
   460  	}
   461  }