spec.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. "encoding/json"
  16. "fmt"
  17. "io/ioutil"
  18. "os"
  19. "path/filepath"
  20. "strings"
  21. "sync"
  22. oci "github.com/opencontainers/runtime-spec/specs-go"
  23. "sigs.k8s.io/yaml"
  24. "github.com/container-orchestrated-devices/container-device-interface/internal/validation"
  25. cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
  26. )
  27. const (
  28. // defaultSpecExt is the file extension for the default encoding.
  29. defaultSpecExt = ".yaml"
  30. )
  31. var (
  32. // Externally set CDI Spec validation function.
  33. specValidator func(*cdi.Spec) error
  34. validatorLock sync.RWMutex
  35. )
  36. // Spec represents a single CDI Spec. It is usually loaded from a
  37. // file and stored in a cache. The Spec has an associated priority.
  38. // This priority is inherited from the associated priority of the
  39. // CDI Spec directory that contains the CDI Spec file and is used
  40. // to resolve conflicts if multiple CDI Spec files contain entries
  41. // for the same fully qualified device.
  42. type Spec struct {
  43. *cdi.Spec
  44. vendor string
  45. class string
  46. path string
  47. priority int
  48. devices map[string]*Device
  49. }
  50. // ReadSpec reads the given CDI Spec file. The resulting Spec is
  51. // assigned the given priority. If reading or parsing the Spec
  52. // data fails ReadSpec returns a nil Spec and an error.
  53. func ReadSpec(path string, priority int) (*Spec, error) {
  54. data, err := ioutil.ReadFile(path)
  55. switch {
  56. case os.IsNotExist(err):
  57. return nil, err
  58. case err != nil:
  59. return nil, fmt.Errorf("failed to read CDI Spec %q: %w", path, err)
  60. }
  61. raw, err := ParseSpec(data)
  62. if err != nil {
  63. return nil, fmt.Errorf("failed to parse CDI Spec %q: %w", path, err)
  64. }
  65. if raw == nil {
  66. return nil, fmt.Errorf("failed to parse CDI Spec %q, no Spec data", path)
  67. }
  68. spec, err := newSpec(raw, path, priority)
  69. if err != nil {
  70. return nil, err
  71. }
  72. return spec, nil
  73. }
  74. // newSpec creates a new Spec from the given CDI Spec data. The
  75. // Spec is marked as loaded from the given path with the given
  76. // priority. If Spec data validation fails newSpec returns a nil
  77. // Spec and an error.
  78. func newSpec(raw *cdi.Spec, path string, priority int) (*Spec, error) {
  79. err := validateSpec(raw)
  80. if err != nil {
  81. return nil, err
  82. }
  83. spec := &Spec{
  84. Spec: raw,
  85. path: filepath.Clean(path),
  86. priority: priority,
  87. }
  88. if ext := filepath.Ext(spec.path); ext != ".yaml" && ext != ".json" {
  89. spec.path += defaultSpecExt
  90. }
  91. spec.vendor, spec.class = ParseQualifier(spec.Kind)
  92. if spec.devices, err = spec.validate(); err != nil {
  93. return nil, fmt.Errorf("invalid CDI Spec: %w", err)
  94. }
  95. return spec, nil
  96. }
  97. // Write the CDI Spec to the file associated with it during instantiation
  98. // by newSpec() or ReadSpec().
  99. func (s *Spec) write(overwrite bool) error {
  100. var (
  101. data []byte
  102. dir string
  103. tmp *os.File
  104. err error
  105. )
  106. err = validateSpec(s.Spec)
  107. if err != nil {
  108. return err
  109. }
  110. if filepath.Ext(s.path) == ".yaml" {
  111. data, err = yaml.Marshal(s.Spec)
  112. data = append([]byte("---\n"), data...)
  113. } else {
  114. data, err = json.Marshal(s.Spec)
  115. }
  116. if err != nil {
  117. return fmt.Errorf("failed to marshal Spec file: %w", err)
  118. }
  119. dir = filepath.Dir(s.path)
  120. err = os.MkdirAll(dir, 0o755)
  121. if err != nil {
  122. return fmt.Errorf("failed to create Spec dir: %w", err)
  123. }
  124. tmp, err = os.CreateTemp(dir, "spec.*.tmp")
  125. if err != nil {
  126. return fmt.Errorf("failed to create Spec file: %w", err)
  127. }
  128. _, err = tmp.Write(data)
  129. tmp.Close()
  130. if err != nil {
  131. return fmt.Errorf("failed to write Spec file: %w", err)
  132. }
  133. err = renameIn(dir, filepath.Base(tmp.Name()), filepath.Base(s.path), overwrite)
  134. if err != nil {
  135. os.Remove(tmp.Name())
  136. err = fmt.Errorf("failed to write Spec file: %w", err)
  137. }
  138. return err
  139. }
  140. // GetVendor returns the vendor of this Spec.
  141. func (s *Spec) GetVendor() string {
  142. return s.vendor
  143. }
  144. // GetClass returns the device class of this Spec.
  145. func (s *Spec) GetClass() string {
  146. return s.class
  147. }
  148. // GetDevice returns the device for the given unqualified name.
  149. func (s *Spec) GetDevice(name string) *Device {
  150. return s.devices[name]
  151. }
  152. // GetPath returns the filesystem path of this Spec.
  153. func (s *Spec) GetPath() string {
  154. return s.path
  155. }
  156. // GetPriority returns the priority of this Spec.
  157. func (s *Spec) GetPriority() int {
  158. return s.priority
  159. }
  160. // ApplyEdits applies the Spec's global-scope container edits to an OCI Spec.
  161. func (s *Spec) ApplyEdits(ociSpec *oci.Spec) error {
  162. return s.edits().Apply(ociSpec)
  163. }
  164. // edits returns the applicable global container edits for this spec.
  165. func (s *Spec) edits() *ContainerEdits {
  166. return &ContainerEdits{&s.ContainerEdits}
  167. }
  168. // Validate the Spec.
  169. func (s *Spec) validate() (map[string]*Device, error) {
  170. if err := validateVersion(s.Version); err != nil {
  171. return nil, err
  172. }
  173. minVersion, err := MinimumRequiredVersion(s.Spec)
  174. if err != nil {
  175. return nil, fmt.Errorf("could not determine minimum required version: %v", err)
  176. }
  177. if newVersion(minVersion).IsGreaterThan(newVersion(s.Version)) {
  178. return nil, fmt.Errorf("the spec version must be at least v%v", minVersion)
  179. }
  180. if err := ValidateVendorName(s.vendor); err != nil {
  181. return nil, err
  182. }
  183. if err := ValidateClassName(s.class); err != nil {
  184. return nil, err
  185. }
  186. if err := validation.ValidateSpecAnnotations(s.Kind, s.Annotations); err != nil {
  187. return nil, err
  188. }
  189. if err := s.edits().Validate(); err != nil {
  190. return nil, err
  191. }
  192. devices := make(map[string]*Device)
  193. for _, d := range s.Devices {
  194. dev, err := newDevice(s, d)
  195. if err != nil {
  196. return nil, fmt.Errorf("failed add device %q: %w", d.Name, err)
  197. }
  198. if _, conflict := devices[d.Name]; conflict {
  199. return nil, fmt.Errorf("invalid spec, multiple device %q", d.Name)
  200. }
  201. devices[d.Name] = dev
  202. }
  203. return devices, nil
  204. }
  205. // validateVersion checks whether the specified spec version is supported.
  206. func validateVersion(version string) error {
  207. if !validSpecVersions.isValidVersion(version) {
  208. return fmt.Errorf("invalid version %q", version)
  209. }
  210. return nil
  211. }
  212. // ParseSpec parses CDI Spec data into a raw CDI Spec.
  213. func ParseSpec(data []byte) (*cdi.Spec, error) {
  214. var raw *cdi.Spec
  215. err := yaml.UnmarshalStrict(data, &raw)
  216. if err != nil {
  217. return nil, fmt.Errorf("failed to unmarshal CDI Spec: %w", err)
  218. }
  219. return raw, nil
  220. }
  221. // SetSpecValidator sets a CDI Spec validator function. This function
  222. // is used for extra CDI Spec content validation whenever a Spec file
  223. // loaded (using ReadSpec() or written (using WriteSpec()).
  224. func SetSpecValidator(fn func(*cdi.Spec) error) {
  225. validatorLock.Lock()
  226. defer validatorLock.Unlock()
  227. specValidator = fn
  228. }
  229. // validateSpec validates the Spec using the extneral validator.
  230. func validateSpec(raw *cdi.Spec) error {
  231. validatorLock.RLock()
  232. defer validatorLock.RUnlock()
  233. if specValidator == nil {
  234. return nil
  235. }
  236. err := specValidator(raw)
  237. if err != nil {
  238. return fmt.Errorf("Spec validation failed: %w", err)
  239. }
  240. return nil
  241. }
  242. // GenerateSpecName generates a vendor+class scoped Spec file name. The
  243. // name can be passed to WriteSpec() to write a Spec file to the file
  244. // system.
  245. //
  246. // vendor and class should match the vendor and class of the CDI Spec.
  247. // The file name is generated without a ".json" or ".yaml" extension.
  248. // The caller can append the desired extension to choose a particular
  249. // encoding. Otherwise WriteSpec() will use its default encoding.
  250. //
  251. // This function always returns the same name for the same vendor/class
  252. // combination. Therefore it cannot be used as such to generate multiple
  253. // Spec file names for a single vendor and class.
  254. func GenerateSpecName(vendor, class string) string {
  255. return vendor + "-" + class
  256. }
  257. // GenerateTransientSpecName generates a vendor+class scoped transient
  258. // Spec file name. The name can be passed to WriteSpec() to write a Spec
  259. // file to the file system.
  260. //
  261. // Transient Specs are those whose lifecycle is tied to that of some
  262. // external entity, for instance a container. vendor and class should
  263. // match the vendor and class of the CDI Spec. transientID should be
  264. // unique among all CDI users on the same host that might generate
  265. // transient Spec files using the same vendor/class combination. If
  266. // the external entity to which the lifecycle of the transient Spec
  267. // is tied to has a unique ID of its own, then this is usually a
  268. // good choice for transientID.
  269. //
  270. // The file name is generated without a ".json" or ".yaml" extension.
  271. // The caller can append the desired extension to choose a particular
  272. // encoding. Otherwise WriteSpec() will use its default encoding.
  273. func GenerateTransientSpecName(vendor, class, transientID string) string {
  274. transientID = strings.ReplaceAll(transientID, "/", "_")
  275. return GenerateSpecName(vendor, class) + "_" + transientID
  276. }
  277. // GenerateNameForSpec generates a name for the given Spec using
  278. // GenerateSpecName with the vendor and class taken from the Spec.
  279. // On success it returns the generated name and a nil error. If
  280. // the Spec does not contain a valid vendor or class, it returns
  281. // an empty name and a non-nil error.
  282. func GenerateNameForSpec(raw *cdi.Spec) (string, error) {
  283. vendor, class := ParseQualifier(raw.Kind)
  284. if vendor == "" {
  285. return "", fmt.Errorf("invalid vendor/class %q in Spec", raw.Kind)
  286. }
  287. return GenerateSpecName(vendor, class), nil
  288. }
  289. // GenerateNameForTransientSpec generates a name for the given transient
  290. // Spec using GenerateTransientSpecName with the vendor and class taken
  291. // from the Spec. On success it returns the generated name and a nil error.
  292. // If the Spec does not contain a valid vendor or class, it returns an
  293. // an empty name and a non-nil error.
  294. func GenerateNameForTransientSpec(raw *cdi.Spec, transientID string) (string, error) {
  295. vendor, class := ParseQualifier(raw.Kind)
  296. if vendor == "" {
  297. return "", fmt.Errorf("invalid vendor/class %q in Spec", raw.Kind)
  298. }
  299. return GenerateTransientSpecName(vendor, class, transientID), nil
  300. }