m/{node,proto}: implement Node Labels

Nodes can now have Labels attached to them. These are string key/value
data designed after Kubernetes labels. They are meant to be used to
attach metadata to nodes, for example external IDs, nicknames or
geographical information.

This change implements just the core functionality: storing them within
etcd, retrieving them via management and curator APIs, and mutating them
via a new management RPC.

Followup changes will impelement provisioning labels at
bootstrap/registration time and accessing label data from metroctl.

Change-Id: I556b452a65061294e7c51037723a6db31d587716
Reviewed-on: https://review.monogon.dev/c/monogon/+/3101
Reviewed-by: Jan Schär <jan@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/node/labels_test.go b/metropolis/node/labels_test.go
new file mode 100644
index 0000000..0e8b799
--- /dev/null
+++ b/metropolis/node/labels_test.go
@@ -0,0 +1,26 @@
+package node
+
+import (
+	"errors"
+	"testing"
+)
+
+func TestValidateLabelKeyValue(t *testing.T) {
+	for i, te := range []struct {
+		in   string
+		want error
+	}{
+		{"foo", nil},
+		{"foo-bar.baz_barfoo", nil},
+		{"-", ErrLabelInvalidFirstCharacter},
+		{"-invalid", ErrLabelInvalidFirstCharacter},
+		{"invalid-", ErrLabelInvalidLastCharacter},
+		{"", ErrLabelEmpty},
+		{"accordingtoallknownlawsofaviationthereisnowaythatabeeshouldbeabletofly", ErrLabelTooLong},
+		{"example.com/annotation", ErrLabelInvalidCharacter},
+	} {
+		if got := ValidateLabel(te.in); !errors.Is(got, te.want) {
+			t.Errorf("%d: wanted %v, got %v", i, te.want, got)
+		}
+	}
+}