| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- package external
- import (
- "fmt"
- "maps"
- "regexp"
- "strings"
- "gopkg.in/yaml.v3"
- )
- // Kubernetes label key/value constraints.
- // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
- var (
- // nameSegment: 1–63 chars, alphanumeric start/end, [-_.] allowed between.
- reNameSegment = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9._-]{0,61}[a-zA-Z0-9])?$`)
- // DNS label: 1–63 chars, alphanumeric start/end, hyphens allowed between.
- reDNSLabel = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
- // label value: empty OR 1–63 chars with same rules as name segment.
- reLabelValue = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9._-]{0,61}[a-zA-Z0-9])?$`)
- )
- // ConfigMapSource implements LabelSource for Kubernetes ConfigMaps.
- type ConfigMapSource struct {
- cfg *Config
- }
- func (cms *ConfigMapSource) ExtractNodeLabels(data map[string]string) (map[string]string, error) {
- if cms.cfg == nil {
- return nil, fmt.Errorf("nil config")
- }
- nlCfg := cms.cfg.NodeLabelConfig()
- if nlCfg == nil {
- return nil, fmt.Errorf("no node label config")
- }
- cm := nlCfg.ConfigMapName()
- key := nlCfg.Key()
- route := nlCfg.Route()
- // Traditional ConfigMap — labels live directly in data.
- if key == "" && route == "" {
- return maps.Clone(data), nil
- }
- // route is optional for block scalar. A root yaml node can be the map of node labels.
- if key == "" && route != "" {
- return nil, fmt.Errorf("key must be set for block scalar configMap")
- }
- // Block-scalar ConfigMap — extract the YAML document from data[Key].
- raw, ok := data[key]
- if !ok {
- return nil, fmt.Errorf("key %q not found in ConfigMap %s", key, cm)
- }
- labels, err := parse(raw, route)
- if err != nil {
- return nil, fmt.Errorf("error parsing the yaml: %w", err)
- }
- // Drop any keys or values that don't satisfy the Kubernetes label spec.
- return filterValidLabels(labels), nil
- }
- // filterValidLabels removes entries from labels whose key or value does not
- // satisfy the Kubernetes label syntax rules. The map is mutated in place.
- func filterValidLabels(labels map[string]string) map[string]string {
- for k, v := range labels {
- if validateLabelKey(k) != nil || validateLabelValue(v) != nil {
- delete(labels, k)
- }
- }
- return labels
- }
- // validateLabelKey checks the optional-prefix/name structure of a label key.
- func validateLabelKey(key string) error {
- if key == "" {
- return fmt.Errorf("label key must not be empty")
- }
- prefix, name, hasSep := strings.Cut(key, "/")
- // No Prefix such as
- // app.kubernetes.io/name
- if !hasSep {
- // No prefix — the whole key is the name segment.
- if err := validateNameSegment(key); err != nil {
- return fmt.Errorf("invalid label key %q: %w", key, err)
- }
- return nil
- }
- if err := validateDNSSubdomain(prefix); err != nil {
- return fmt.Errorf("invalid label key %q: prefix is not a valid DNS subdomain: %w", key, err)
- }
- if err := validateNameSegment(name); err != nil {
- return fmt.Errorf("invalid label key %q: name segment: %w", key, err)
- }
- return nil
- }
- // validateNameSegment checks the name part of a label key (up to 63 chars).
- func validateNameSegment(name string) error {
- if name == "" {
- return fmt.Errorf("name segment must not be empty")
- }
- if len(name) > 63 {
- return fmt.Errorf("name segment %q exceeds 63 characters", name)
- }
- if !reNameSegment.MatchString(name) {
- return fmt.Errorf("name segment %q must begin and end with an alphanumeric character and may only contain [-_.]", name)
- }
- return nil
- }
- // validateDNSSubdomain checks that s is a valid DNS subdomain (≤253 chars,
- // dot-separated DNS labels each ≤63 chars).
- func validateDNSSubdomain(s string) error {
- if s == "" {
- return fmt.Errorf("DNS subdomain must not be empty")
- }
- if len(s) > 253 {
- return fmt.Errorf("DNS subdomain %q exceeds 253 characters", s)
- }
- for _, label := range strings.Split(s, ".") {
- if label == "" {
- return fmt.Errorf("DNS subdomain %q contains an empty label (consecutive or trailing dots)", s)
- }
- if len(label) > 63 {
- return fmt.Errorf("DNS subdomain %q: label %q exceeds 63 characters", s, label)
- }
- if !reDNSLabel.MatchString(label) {
- return fmt.Errorf("DNS subdomain %q: label %q must begin and end with an alphanumeric character and may only contain hyphens", s, label)
- }
- }
- return nil
- }
- // validateLabelValue checks a label value (empty is allowed; otherwise ≤63 chars).
- func validateLabelValue(value string) error {
- if value == "" {
- return nil
- }
- if len(value) > 63 {
- return fmt.Errorf("label value %q exceeds 63 characters", value)
- }
- if !reLabelValue.MatchString(value) {
- return fmt.Errorf("label value %q must begin and end with an alphanumeric character and may only contain [-_.]", value)
- }
- return nil
- }
- func parseNormally(input []byte) (map[string]string, error) {
- var m map[string]string
- err := yaml.Unmarshal(input, &m)
- if err != nil {
- return nil, fmt.Errorf("failed to parse yaml: %w", err)
- }
- return m, nil
- }
- func parseRoute(input []byte, routes []string) (map[string]string, error) {
- // 1. parse as map[string]any
- var m map[string]any
- err := yaml.Unmarshal(input, &m)
- if err != nil {
- return nil, fmt.Errorf("failed to parse root yaml: %w", err)
- }
- // 2. traverse the yaml based on the route. error if any of the routes don't exist
- for _, route := range routes {
- value, ok := m[route]
- if !ok {
- return nil, fmt.Errorf("failed to locate route: %s within yaml", route)
- }
- innerMap, ok := value.(map[string]any)
- if !ok {
- return nil, fmt.Errorf("route at %s is not a map", route)
- }
- m = innerMap
- }
- // 3. Now that we've traversed the route, our `m` value can be marshalled back into yaml,
- // and then unmarshalled regularly
- targetBytes, err := yaml.Marshal(m)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal route yaml block: %w", err)
- }
- return parseNormally(targetBytes)
- }
- func parse(yamlData string, routeStr string) (map[string]string, error) {
- // do all the validation stuff ...
- input := []byte(yamlData)
- routeStr = strings.TrimSpace(routeStr)
- if routeStr == "" {
- // No route provided; parse the root YAML as the labels map.
- return parseNormally(input)
- }
- // Split routes and drop any empty segments (e.g. leading/trailing dots).
- routes := strings.Split(routeStr, ".")
- // no routes, just parse yaml as is
- if len(routes) == 0 {
- return parseNormally(input)
- }
- // when there are empty segments error out
- // Eg: external..labels
- for _, r := range routes {
- if r == "" {
- return nil, fmt.Errorf("invalid route %q: empty segment found", routeStr)
- }
- }
- // parse with routes
- return parseRoute(input, routes)
- }
|