metropolis: support prefixes in node labels

This brings Metropolis node label semantics to be the same as Kubernetes
labels.

Change-Id: I33c321432ec01abf978bb8dfbb3cef90f75a38eb
Reviewed-on: https://review.monogon.dev/c/monogon/+/3467
Tested-by: Jenkins CI
Reviewed-by: Jan Schär <jan@monogon.tech>
diff --git a/metropolis/node/labels.go b/metropolis/node/labels.go
index f26e5bd..93f1551 100644
--- a/metropolis/node/labels.go
+++ b/metropolis/node/labels.go
@@ -3,6 +3,9 @@
 import (
 	"fmt"
 	"regexp"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/util/validation"
 
 	cpb "source.monogon.dev/metropolis/proto/common"
 )
@@ -26,6 +29,8 @@
 	// ErrLabelInvalidCharacter is returned by ValidateLabel if the label key/value
 	// contains an invalid character.
 	ErrLabelInvalidCharacter = fmt.Errorf("invalid character")
+	ErrLabelEmptyPrefix      = fmt.Errorf("empty prefix")
+	ErrLabelInvalidPrefix    = fmt.Errorf("invalid prefix")
 )
 
 const (
@@ -34,20 +39,46 @@
 	MaxLabelsPerNode = 128
 )
 
-// ValidateLabel ensures that a given node label key/value component is valid:
+func validatePrefix(prefix string) error {
+	if prefix == "" {
+		return ErrLabelEmptyPrefix
+	}
+	if errs := validation.IsDNS1123Subdomain(prefix); len(errs) > 0 {
+		return ErrLabelInvalidPrefix
+	}
+	return nil
+}
+
+// ValidateLabelKey ensures that a given node label key is valid:
 //
 //  1. 1 to 63 characters long (inclusive);
 //  2. Characters are all ASCII a-z A-Z 0-9 '_', '-' or '.';
 //  3. The first character is ASCII a-z A-Z or 0-9.
 //  4. The last character is ASCII a-z A-Z or 0-9.
+//  5. Optional slash-delimited prefix which contains a valid 'DNS subdomain'.
 //
 // If it's valid, nil is returned. Otherwise, one of ErrLabelEmpty,
 // ErrLabelTooLong, ErrLabelInvalidFirstCharacter or ErrLabelInvalidCharacter is
 // returned.
-func ValidateLabel(v string) error {
+func ValidateLabelKey(v string) error {
+	// Split away prefix.
+	parts := strings.Split(v, "/")
+	switch len(parts) {
+	case 1:
+	case 2:
+		prefix := parts[0]
+		if err := validatePrefix(prefix); err != nil {
+			return err
+		}
+		v = parts[1]
+	default:
+		return ErrLabelInvalidPrefix
+	}
+
 	if len(v) == 0 {
 		return ErrLabelEmpty
 	}
+
 	if len(v) > 63 {
 		return ErrLabelTooLong
 	}
@@ -65,6 +96,37 @@
 	return nil
 }
 
+// ValidateLabelValue ensures that a given node label value is valid:
+//
+//  1. 0 to 63 characters long (inclusive);
+//  2. Characters are all ASCII a-z A-Z 0-9 '_', '-' or '.';
+//  3. The first character is ASCII a-z A-Z or 0-9.
+//  4. The last character is ASCII a-z A-Z or 0-9.
+//
+// If it's valid, nil is returned. Otherwise, one of ErrLabelTooLong,
+// ErrLabelInvalidFirstCharacter, or ErrLabelInvalidLastCharacter,
+// ErrLabelInvalidCharacter is returned.
+func ValidateLabelValue(v string) error {
+	if len(v) == 0 {
+		return nil
+	}
+	if len(v) > 63 {
+		return ErrLabelTooLong
+	}
+	if !reLabelFirstLast.MatchString(string(v[0])) {
+		return ErrLabelInvalidFirstCharacter
+	}
+	if !reLabelFirstLast.MatchString(string(v[len(v)-1])) {
+		return ErrLabelInvalidLastCharacter
+	}
+	// Body characters are a superset of the first character, and we've already
+	// checked that so we can check the entire string here.
+	if !reLabelBody.MatchString(v) {
+		return ErrLabelInvalidCharacter
+	}
+	return nil
+}
+
 // GetNodeLabel retrieves a node label by key, returning its value or an empty
 // string if no labels with this key is set on the node.
 func GetNodeLabel(labels *cpb.NodeLabels, key string) string {