m/node: allow specifying node labels during node registration

Change-Id: Ie7fc7387314cd2f59661c2d07530b712f8f29b48
Reviewed-on: https://review.monogon.dev/c/monogon/+/3104
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/node/core/cluster/cluster_register.go b/metropolis/node/core/cluster/cluster_register.go
index bac85db..2389eaf 100644
--- a/metropolis/node/core/cluster/cluster_register.go
+++ b/metropolis/node/core/cluster/cluster_register.go
@@ -122,6 +122,7 @@
 		RegisterTicket: register.RegisterTicket,
 		JoinKey:        jpub,
 		HaveLocalTpm:   m.haveTPM,
+		Labels:         register.Labels,
 	})
 	if err != nil {
 		return fmt.Errorf("register call failed: %w", err)
diff --git a/metropolis/node/core/curator/impl_leader_curator.go b/metropolis/node/core/curator/impl_leader_curator.go
index b88525f..d81f4eb 100644
--- a/metropolis/node/core/curator/impl_leader_curator.go
+++ b/metropolis/node/core/curator/impl_leader_curator.go
@@ -365,12 +365,40 @@
 		return nil, err
 	}
 
+	// Populate node labels if applicable.
+	labels := make(map[string]string)
+	if l := req.Labels; l != nil {
+		if nlabels := len(l.Pairs); nlabels > common.MaxLabelsPerNode {
+			rpc.Trace(ctx).Printf("Too many labels (%d, limit %d), truncating...", nlabels, common.MaxLabelsPerNode)
+			l.Pairs = l.Pairs[:common.MaxLabelsPerNode]
+		}
+		for _, pair := range l.Pairs {
+			k := pair.Key
+			v := pair.Value
+
+			if err := common.ValidateLabel(k); err != nil {
+				rpc.Trace(ctx).Printf("Label key %q is invalid: %v, skipping", k, err)
+				continue
+			}
+			if err := common.ValidateLabel(v); err != nil {
+				rpc.Trace(ctx).Printf("Label value %q (key %q) is invalid: %v, skipping", v, k, err)
+				continue
+			}
+			if _, ok := labels[k]; ok {
+				rpc.Trace(ctx).Printf("Label key %q is duplicate, skipping", k)
+				continue
+			}
+			labels[k] = v
+		}
+	}
+
 	// No node exists, create one.
 	node = &Node{
 		pubkey:   pubkey,
 		jkey:     req.JoinKey,
 		state:    cpb.NodeState_NODE_STATE_NEW,
 		tpmUsage: tpmUsage,
+		labels:   labels,
 	}
 	if err := nodeSave(ctx, l.leadership, node); err != nil {
 		return nil, err
diff --git a/metropolis/node/core/curator/proto/api/api.proto b/metropolis/node/core/curator/proto/api/api.proto
index ef7e24c..9641c24 100644
--- a/metropolis/node/core/curator/proto/api/api.proto
+++ b/metropolis/node/core/curator/proto/api/api.proto
@@ -271,6 +271,7 @@
     // successfully initialized. This information should be verified by the
     // Curator in high-assurance scenarios using hardware attestation.
     bool have_local_tpm = 3;
+    metropolis.proto.common.NodeLabels labels = 4;
 }
 
 message RegisterNodeResponse {
diff --git a/metropolis/proto/api/configuration.proto b/metropolis/proto/api/configuration.proto
index 90e9dbd..ba8a7f3 100644
--- a/metropolis/proto/api/configuration.proto
+++ b/metropolis/proto/api/configuration.proto
@@ -68,6 +68,11 @@
         // attempting to register into a cluster. It can be retrieved by
         // an operator from a running cluster via Management.GetClusterInfo.
         bytes ca_certificate = 3;
+
+        // Labels that the new node will start out with. The given labels must
+        // be valid (see NodeLabels for more details). Invalid labels will be
+        // discarded.
+        metropolis.proto.common.NodeLabels labels = 4;
     }
     oneof cluster {
         ClusterBootstrap cluster_bootstrap = 1;
diff --git a/metropolis/test/launch/cluster/cluster.go b/metropolis/test/launch/cluster/cluster.go
index 9bce17c..4cbfede 100644
--- a/metropolis/test/launch/cluster/cluster.go
+++ b/metropolis/test/launch/cluster/cluster.go
@@ -967,6 +967,11 @@
 						RegisterTicket:   ticket,
 						ClusterDirectory: resI.ClusterDirectory,
 						CaCertificate:    resI.CaCertificate,
+						Labels: &cpb.NodeLabels{
+							Pairs: []*cpb.NodeLabels_Pair{
+								{Key: "test-node-id", Value: fmt.Sprintf("%d", i)},
+							},
+						},
 					},
 				},
 			},