runtime_unix_test.go 12 KB

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