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 }