annotations.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. /*
  2. Copyright © 2021-2022 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. "strings"
  18. "github.com/container-orchestrated-devices/container-device-interface/pkg/parser"
  19. )
  20. const (
  21. // AnnotationPrefix is the prefix for CDI container annotation keys.
  22. AnnotationPrefix = "cdi.k8s.io/"
  23. )
  24. // UpdateAnnotations updates annotations with a plugin-specific CDI device
  25. // injection request for the given devices. Upon any error a non-nil error
  26. // is returned and annotations are left intact. By convention plugin should
  27. // be in the format of "vendor.device-type".
  28. func UpdateAnnotations(annotations map[string]string, plugin string, deviceID string, devices []string) (map[string]string, error) {
  29. key, err := AnnotationKey(plugin, deviceID)
  30. if err != nil {
  31. return annotations, fmt.Errorf("CDI annotation failed: %w", err)
  32. }
  33. if _, ok := annotations[key]; ok {
  34. return annotations, fmt.Errorf("CDI annotation failed, key %q used", key)
  35. }
  36. value, err := AnnotationValue(devices)
  37. if err != nil {
  38. return annotations, fmt.Errorf("CDI annotation failed: %w", err)
  39. }
  40. if annotations == nil {
  41. annotations = make(map[string]string)
  42. }
  43. annotations[key] = value
  44. return annotations, nil
  45. }
  46. // ParseAnnotations parses annotations for CDI device injection requests.
  47. // The keys and devices from all such requests are collected into slices
  48. // which are returned as the result. All devices are expected to be fully
  49. // qualified CDI device names. If any device fails this check empty slices
  50. // are returned along with a non-nil error. The annotations are expected
  51. // to be formatted by, or in a compatible fashion to UpdateAnnotations().
  52. func ParseAnnotations(annotations map[string]string) ([]string, []string, error) {
  53. var (
  54. keys []string
  55. devices []string
  56. )
  57. for key, value := range annotations {
  58. if !strings.HasPrefix(key, AnnotationPrefix) {
  59. continue
  60. }
  61. for _, d := range strings.Split(value, ",") {
  62. if !IsQualifiedName(d) {
  63. return nil, nil, fmt.Errorf("invalid CDI device name %q", d)
  64. }
  65. devices = append(devices, d)
  66. }
  67. keys = append(keys, key)
  68. }
  69. return keys, devices, nil
  70. }
  71. // AnnotationKey returns a unique annotation key for an device allocation
  72. // by a K8s device plugin. pluginName should be in the format of
  73. // "vendor.device-type". deviceID is the ID of the device the plugin is
  74. // allocating. It is used to make sure that the generated key is unique
  75. // even if multiple allocations by a single plugin needs to be annotated.
  76. func AnnotationKey(pluginName, deviceID string) (string, error) {
  77. const maxNameLen = 63
  78. if pluginName == "" {
  79. return "", errors.New("invalid plugin name, empty")
  80. }
  81. if deviceID == "" {
  82. return "", errors.New("invalid deviceID, empty")
  83. }
  84. name := pluginName + "_" + strings.ReplaceAll(deviceID, "/", "_")
  85. if len(name) > maxNameLen {
  86. return "", fmt.Errorf("invalid plugin+deviceID %q, too long", name)
  87. }
  88. if c := rune(name[0]); !parser.IsAlphaNumeric(c) {
  89. return "", fmt.Errorf("invalid name %q, first '%c' should be alphanumeric",
  90. name, c)
  91. }
  92. if len(name) > 2 {
  93. for _, c := range name[1 : len(name)-1] {
  94. switch {
  95. case parser.IsAlphaNumeric(c):
  96. case c == '_' || c == '-' || c == '.':
  97. default:
  98. return "", fmt.Errorf("invalid name %q, invalid character '%c'",
  99. name, c)
  100. }
  101. }
  102. }
  103. if c := rune(name[len(name)-1]); !parser.IsAlphaNumeric(c) {
  104. return "", fmt.Errorf("invalid name %q, last '%c' should be alphanumeric",
  105. name, c)
  106. }
  107. return AnnotationPrefix + name, nil
  108. }
  109. // AnnotationValue returns an annotation value for the given devices.
  110. func AnnotationValue(devices []string) (string, error) {
  111. value, sep := "", ""
  112. for _, d := range devices {
  113. if _, _, _, err := ParseQualifiedName(d); err != nil {
  114. return "", err
  115. }
  116. value += sep + d
  117. sep = ","
  118. }
  119. return value, nil
  120. }