runtime_unix_test.go 11 KB

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