2
0

configmapsource.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. package external
  2. import (
  3. "fmt"
  4. "maps"
  5. "regexp"
  6. "strings"
  7. "gopkg.in/yaml.v3"
  8. )
  9. // Kubernetes label key/value constraints.
  10. // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
  11. var (
  12. // nameSegment: 1–63 chars, alphanumeric start/end, [-_.] allowed between.
  13. reNameSegment = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9._-]{0,61}[a-zA-Z0-9])?$`)
  14. // DNS label: 1–63 chars, alphanumeric start/end, hyphens allowed between.
  15. reDNSLabel = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
  16. // label value: empty OR 1–63 chars with same rules as name segment.
  17. reLabelValue = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9._-]{0,61}[a-zA-Z0-9])?$`)
  18. )
  19. // ConfigMapSource implements LabelSource for Kubernetes ConfigMaps.
  20. type ConfigMapSource struct {
  21. cfg *Config
  22. }
  23. func (cms *ConfigMapSource) ExtractNodeLabels(data map[string]string) (map[string]string, error) {
  24. if cms.cfg == nil {
  25. return nil, fmt.Errorf("nil config")
  26. }
  27. nlCfg := cms.cfg.NodeLabelConfig()
  28. if nlCfg == nil {
  29. return nil, fmt.Errorf("no node label config")
  30. }
  31. cm := nlCfg.ConfigMapName()
  32. key := nlCfg.Key()
  33. route := nlCfg.Route()
  34. // Traditional ConfigMap — labels live directly in data.
  35. if key == "" && route == "" {
  36. return maps.Clone(data), nil
  37. }
  38. // route is optional for block scalar. A root yaml node can be the map of node labels.
  39. if key == "" && route != "" {
  40. return nil, fmt.Errorf("key must be set for block scalar configMap")
  41. }
  42. // Block-scalar ConfigMap — extract the YAML document from data[Key].
  43. raw, ok := data[key]
  44. if !ok {
  45. return nil, fmt.Errorf("key %q not found in ConfigMap %s", key, cm)
  46. }
  47. labels, err := parse(raw, route)
  48. if err != nil {
  49. return nil, fmt.Errorf("error parsing the yaml: %w", err)
  50. }
  51. // Drop any keys or values that don't satisfy the Kubernetes label spec.
  52. return filterValidLabels(labels), nil
  53. }
  54. // filterValidLabels removes entries from labels whose key or value does not
  55. // satisfy the Kubernetes label syntax rules. The map is mutated in place.
  56. func filterValidLabels(labels map[string]string) map[string]string {
  57. for k, v := range labels {
  58. if validateLabelKey(k) != nil || validateLabelValue(v) != nil {
  59. delete(labels, k)
  60. }
  61. }
  62. return labels
  63. }
  64. // validateLabelKey checks the optional-prefix/name structure of a label key.
  65. func validateLabelKey(key string) error {
  66. if key == "" {
  67. return fmt.Errorf("label key must not be empty")
  68. }
  69. prefix, name, hasSep := strings.Cut(key, "/")
  70. // No Prefix such as
  71. // app.kubernetes.io/name
  72. if !hasSep {
  73. // No prefix — the whole key is the name segment.
  74. if err := validateNameSegment(key); err != nil {
  75. return fmt.Errorf("invalid label key %q: %w", key, err)
  76. }
  77. return nil
  78. }
  79. if err := validateDNSSubdomain(prefix); err != nil {
  80. return fmt.Errorf("invalid label key %q: prefix is not a valid DNS subdomain: %w", key, err)
  81. }
  82. if err := validateNameSegment(name); err != nil {
  83. return fmt.Errorf("invalid label key %q: name segment: %w", key, err)
  84. }
  85. return nil
  86. }
  87. // validateNameSegment checks the name part of a label key (up to 63 chars).
  88. func validateNameSegment(name string) error {
  89. if name == "" {
  90. return fmt.Errorf("name segment must not be empty")
  91. }
  92. if len(name) > 63 {
  93. return fmt.Errorf("name segment %q exceeds 63 characters", name)
  94. }
  95. if !reNameSegment.MatchString(name) {
  96. return fmt.Errorf("name segment %q must begin and end with an alphanumeric character and may only contain [-_.]", name)
  97. }
  98. return nil
  99. }
  100. // validateDNSSubdomain checks that s is a valid DNS subdomain (≤253 chars,
  101. // dot-separated DNS labels each ≤63 chars).
  102. func validateDNSSubdomain(s string) error {
  103. if s == "" {
  104. return fmt.Errorf("DNS subdomain must not be empty")
  105. }
  106. if len(s) > 253 {
  107. return fmt.Errorf("DNS subdomain %q exceeds 253 characters", s)
  108. }
  109. for _, label := range strings.Split(s, ".") {
  110. if label == "" {
  111. return fmt.Errorf("DNS subdomain %q contains an empty label (consecutive or trailing dots)", s)
  112. }
  113. if len(label) > 63 {
  114. return fmt.Errorf("DNS subdomain %q: label %q exceeds 63 characters", s, label)
  115. }
  116. if !reDNSLabel.MatchString(label) {
  117. return fmt.Errorf("DNS subdomain %q: label %q must begin and end with an alphanumeric character and may only contain hyphens", s, label)
  118. }
  119. }
  120. return nil
  121. }
  122. // validateLabelValue checks a label value (empty is allowed; otherwise ≤63 chars).
  123. func validateLabelValue(value string) error {
  124. if value == "" {
  125. return nil
  126. }
  127. if len(value) > 63 {
  128. return fmt.Errorf("label value %q exceeds 63 characters", value)
  129. }
  130. if !reLabelValue.MatchString(value) {
  131. return fmt.Errorf("label value %q must begin and end with an alphanumeric character and may only contain [-_.]", value)
  132. }
  133. return nil
  134. }
  135. func parseNormally(input []byte) (map[string]string, error) {
  136. var m map[string]string
  137. err := yaml.Unmarshal(input, &m)
  138. if err != nil {
  139. return nil, fmt.Errorf("failed to parse yaml: %w", err)
  140. }
  141. return m, nil
  142. }
  143. func parseRoute(input []byte, routes []string) (map[string]string, error) {
  144. // 1. parse as map[string]any
  145. var m map[string]any
  146. err := yaml.Unmarshal(input, &m)
  147. if err != nil {
  148. return nil, fmt.Errorf("failed to parse root yaml: %w", err)
  149. }
  150. // 2. traverse the yaml based on the route. error if any of the routes don't exist
  151. for _, route := range routes {
  152. value, ok := m[route]
  153. if !ok {
  154. return nil, fmt.Errorf("failed to locate route: %s within yaml", route)
  155. }
  156. innerMap, ok := value.(map[string]any)
  157. if !ok {
  158. return nil, fmt.Errorf("route at %s is not a map", route)
  159. }
  160. m = innerMap
  161. }
  162. // 3. Now that we've traversed the route, our `m` value can be marshalled back into yaml,
  163. // and then unmarshalled regularly
  164. targetBytes, err := yaml.Marshal(m)
  165. if err != nil {
  166. return nil, fmt.Errorf("failed to marshal route yaml block: %w", err)
  167. }
  168. return parseNormally(targetBytes)
  169. }
  170. func parse(yamlData string, routeStr string) (map[string]string, error) {
  171. // do all the validation stuff ...
  172. input := []byte(yamlData)
  173. routeStr = strings.TrimSpace(routeStr)
  174. if routeStr == "" {
  175. // No route provided; parse the root YAML as the labels map.
  176. return parseNormally(input)
  177. }
  178. // Split routes and drop any empty segments (e.g. leading/trailing dots).
  179. routes := strings.Split(routeStr, ".")
  180. // no routes, just parse yaml as is
  181. if len(routes) == 0 {
  182. return parseNormally(input)
  183. }
  184. // when there are empty segments error out
  185. // Eg: external..labels
  186. for _, r := range routes {
  187. if r == "" {
  188. return nil, fmt.Errorf("invalid route %q: empty segment found", routeStr)
  189. }
  190. }
  191. // parse with routes
  192. return parseRoute(input, routes)
  193. }