container-edits.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /*
  2. Copyright © 2021 The CDI Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package cdi
  14. import (
  15. "errors"
  16. "fmt"
  17. "os"
  18. "path/filepath"
  19. "sort"
  20. "strings"
  21. "github.com/container-orchestrated-devices/container-device-interface/specs-go"
  22. oci "github.com/opencontainers/runtime-spec/specs-go"
  23. ocigen "github.com/opencontainers/runtime-tools/generate"
  24. )
  25. const (
  26. // PrestartHook is the name of the OCI "prestart" hook.
  27. PrestartHook = "prestart"
  28. // CreateRuntimeHook is the name of the OCI "createRuntime" hook.
  29. CreateRuntimeHook = "createRuntime"
  30. // CreateContainerHook is the name of the OCI "createContainer" hook.
  31. CreateContainerHook = "createContainer"
  32. // StartContainerHook is the name of the OCI "startContainer" hook.
  33. StartContainerHook = "startContainer"
  34. // PoststartHook is the name of the OCI "poststart" hook.
  35. PoststartHook = "poststart"
  36. // PoststopHook is the name of the OCI "poststop" hook.
  37. PoststopHook = "poststop"
  38. )
  39. var (
  40. // Names of recognized hooks.
  41. validHookNames = map[string]struct{}{
  42. PrestartHook: {},
  43. CreateRuntimeHook: {},
  44. CreateContainerHook: {},
  45. StartContainerHook: {},
  46. PoststartHook: {},
  47. PoststopHook: {},
  48. }
  49. )
  50. // ContainerEdits represent updates to be applied to an OCI Spec.
  51. // These updates can be specific to a CDI device, or they can be
  52. // specific to a CDI Spec. In the former case these edits should
  53. // be applied to all OCI Specs where the corresponding CDI device
  54. // is injected. In the latter case, these edits should be applied
  55. // to all OCI Specs where at least one devices from the CDI Spec
  56. // is injected.
  57. type ContainerEdits struct {
  58. *specs.ContainerEdits
  59. }
  60. // Apply edits to the given OCI Spec. Updates the OCI Spec in place.
  61. // Returns an error if the update fails.
  62. func (e *ContainerEdits) Apply(spec *oci.Spec) error {
  63. if spec == nil {
  64. return errors.New("can't edit nil OCI Spec")
  65. }
  66. if e == nil || e.ContainerEdits == nil {
  67. return nil
  68. }
  69. specgen := ocigen.NewFromSpec(spec)
  70. if len(e.Env) > 0 {
  71. specgen.AddMultipleProcessEnv(e.Env)
  72. }
  73. for _, d := range e.DeviceNodes {
  74. dn := DeviceNode{d}
  75. err := dn.fillMissingInfo()
  76. if err != nil {
  77. return err
  78. }
  79. dev := d.ToOCI()
  80. if dev.UID == nil && spec.Process != nil {
  81. if uid := spec.Process.User.UID; uid > 0 {
  82. dev.UID = &uid
  83. }
  84. }
  85. if dev.GID == nil && spec.Process != nil {
  86. if gid := spec.Process.User.GID; gid > 0 {
  87. dev.GID = &gid
  88. }
  89. }
  90. specgen.RemoveDevice(dev.Path)
  91. specgen.AddDevice(dev)
  92. if dev.Type == "b" || dev.Type == "c" {
  93. access := d.Permissions
  94. if access == "" {
  95. access = "rwm"
  96. }
  97. specgen.AddLinuxResourcesDevice(true, dev.Type, &dev.Major, &dev.Minor, access)
  98. }
  99. }
  100. if len(e.Mounts) > 0 {
  101. for _, m := range e.Mounts {
  102. specgen.RemoveMount(m.ContainerPath)
  103. specgen.AddMount(m.ToOCI())
  104. }
  105. sortMounts(&specgen)
  106. }
  107. for _, h := range e.Hooks {
  108. switch h.HookName {
  109. case PrestartHook:
  110. specgen.AddPreStartHook(h.ToOCI())
  111. case PoststartHook:
  112. specgen.AddPostStartHook(h.ToOCI())
  113. case PoststopHook:
  114. specgen.AddPostStopHook(h.ToOCI())
  115. // TODO: Maybe runtime-tools/generate should be updated with these...
  116. case CreateRuntimeHook:
  117. ensureOCIHooks(spec)
  118. spec.Hooks.CreateRuntime = append(spec.Hooks.CreateRuntime, h.ToOCI())
  119. case CreateContainerHook:
  120. ensureOCIHooks(spec)
  121. spec.Hooks.CreateContainer = append(spec.Hooks.CreateContainer, h.ToOCI())
  122. case StartContainerHook:
  123. ensureOCIHooks(spec)
  124. spec.Hooks.StartContainer = append(spec.Hooks.StartContainer, h.ToOCI())
  125. default:
  126. return fmt.Errorf("unknown hook name %q", h.HookName)
  127. }
  128. }
  129. return nil
  130. }
  131. // Validate container edits.
  132. func (e *ContainerEdits) Validate() error {
  133. if e == nil || e.ContainerEdits == nil {
  134. return nil
  135. }
  136. if err := ValidateEnv(e.Env); err != nil {
  137. return fmt.Errorf("invalid container edits: %w", err)
  138. }
  139. for _, d := range e.DeviceNodes {
  140. if err := (&DeviceNode{d}).Validate(); err != nil {
  141. return err
  142. }
  143. }
  144. for _, h := range e.Hooks {
  145. if err := (&Hook{h}).Validate(); err != nil {
  146. return err
  147. }
  148. }
  149. for _, m := range e.Mounts {
  150. if err := (&Mount{m}).Validate(); err != nil {
  151. return err
  152. }
  153. }
  154. return nil
  155. }
  156. // Append other edits into this one. If called with a nil receiver,
  157. // allocates and returns newly allocated edits.
  158. func (e *ContainerEdits) Append(o *ContainerEdits) *ContainerEdits {
  159. if o == nil || o.ContainerEdits == nil {
  160. return e
  161. }
  162. if e == nil {
  163. e = &ContainerEdits{}
  164. }
  165. if e.ContainerEdits == nil {
  166. e.ContainerEdits = &specs.ContainerEdits{}
  167. }
  168. e.Env = append(e.Env, o.Env...)
  169. e.DeviceNodes = append(e.DeviceNodes, o.DeviceNodes...)
  170. e.Hooks = append(e.Hooks, o.Hooks...)
  171. e.Mounts = append(e.Mounts, o.Mounts...)
  172. return e
  173. }
  174. // isEmpty returns true if these edits are empty. This is valid in a
  175. // global Spec context but invalid in a Device context.
  176. func (e *ContainerEdits) isEmpty() bool {
  177. if e == nil {
  178. return false
  179. }
  180. return len(e.Env)+len(e.DeviceNodes)+len(e.Hooks)+len(e.Mounts) == 0
  181. }
  182. // ValidateEnv validates the given environment variables.
  183. func ValidateEnv(env []string) error {
  184. for _, v := range env {
  185. if strings.IndexByte(v, byte('=')) <= 0 {
  186. return fmt.Errorf("invalid environment variable %q", v)
  187. }
  188. }
  189. return nil
  190. }
  191. // DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes.
  192. type DeviceNode struct {
  193. *specs.DeviceNode
  194. }
  195. // Validate a CDI Spec DeviceNode.
  196. func (d *DeviceNode) Validate() error {
  197. validTypes := map[string]struct{}{
  198. "": {},
  199. "b": {},
  200. "c": {},
  201. "u": {},
  202. "p": {},
  203. }
  204. if d.Path == "" {
  205. return errors.New("invalid (empty) device path")
  206. }
  207. if _, ok := validTypes[d.Type]; !ok {
  208. return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
  209. }
  210. for _, bit := range d.Permissions {
  211. if bit != 'r' && bit != 'w' && bit != 'm' {
  212. return fmt.Errorf("device %q: invalid permissions %q",
  213. d.Path, d.Permissions)
  214. }
  215. }
  216. return nil
  217. }
  218. // Hook is a CDI Spec Hook wrapper, used for validating hooks.
  219. type Hook struct {
  220. *specs.Hook
  221. }
  222. // Validate a hook.
  223. func (h *Hook) Validate() error {
  224. if _, ok := validHookNames[h.HookName]; !ok {
  225. return fmt.Errorf("invalid hook name %q", h.HookName)
  226. }
  227. if h.Path == "" {
  228. return fmt.Errorf("invalid hook %q with empty path", h.HookName)
  229. }
  230. if err := ValidateEnv(h.Env); err != nil {
  231. return fmt.Errorf("invalid hook %q: %w", h.HookName, err)
  232. }
  233. return nil
  234. }
  235. // Mount is a CDI Mount wrapper, used for validating mounts.
  236. type Mount struct {
  237. *specs.Mount
  238. }
  239. // Validate a mount.
  240. func (m *Mount) Validate() error {
  241. if m.HostPath == "" {
  242. return errors.New("invalid mount, empty host path")
  243. }
  244. if m.ContainerPath == "" {
  245. return errors.New("invalid mount, empty container path")
  246. }
  247. return nil
  248. }
  249. // Ensure OCI Spec hooks are not nil so we can add hooks.
  250. func ensureOCIHooks(spec *oci.Spec) {
  251. if spec.Hooks == nil {
  252. spec.Hooks = &oci.Hooks{}
  253. }
  254. }
  255. // sortMounts sorts the mounts in the given OCI Spec.
  256. func sortMounts(specgen *ocigen.Generator) {
  257. mounts := specgen.Mounts()
  258. specgen.ClearMounts()
  259. sort.Sort(orderedMounts(mounts))
  260. specgen.Config.Mounts = mounts
  261. }
  262. // orderedMounts defines how to sort an OCI Spec Mount slice.
  263. // This is the almost the same implementation sa used by CRI-O and Docker,
  264. // with a minor tweak for stable sorting order (easier to test):
  265. //
  266. // https://github.com/moby/moby/blob/17.05.x/daemon/volumes.go#L26
  267. type orderedMounts []oci.Mount
  268. // Len returns the number of mounts. Used in sorting.
  269. func (m orderedMounts) Len() int {
  270. return len(m)
  271. }
  272. // Less returns true if the number of parts (a/b/c would be 3 parts) in the
  273. // mount indexed by parameter 1 is less than that of the mount indexed by
  274. // parameter 2. Used in sorting.
  275. func (m orderedMounts) Less(i, j int) bool {
  276. ip, jp := m.parts(i), m.parts(j)
  277. if ip < jp {
  278. return true
  279. }
  280. if jp < ip {
  281. return false
  282. }
  283. return m[i].Destination < m[j].Destination
  284. }
  285. // Swap swaps two items in an array of mounts. Used in sorting
  286. func (m orderedMounts) Swap(i, j int) {
  287. m[i], m[j] = m[j], m[i]
  288. }
  289. // parts returns the number of parts in the destination of a mount. Used in sorting.
  290. func (m orderedMounts) parts(i int) int {
  291. return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator))
  292. }