runtime_unix_test.go 12 KB

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