metropolis/node: add version to status

This implements submitting the Node's version to its' Status report to
the control plane.

This version is then displayed to the user in metroctl.

Change-Id: I70eadb9a7001b6e50931245e8a6274da2fbdc5bc
Reviewed-on: https://review.monogon.dev/c/monogon/+/2334
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index e3044d5..40d97b7 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -51,6 +51,7 @@
         "//metropolis/pkg/logtree",
         "//metropolis/proto/api",
         "//metropolis/proto/common",
+        "//version",
         "@com_github_adrg_xdg//:xdg",
         "@com_github_spf13_cobra//:cobra",
         "@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
diff --git a/metropolis/cli/metroctl/table_node.go b/metropolis/cli/metroctl/table_node.go
index 07f994c..baa8776 100644
--- a/metropolis/cli/metroctl/table_node.go
+++ b/metropolis/cli/metroctl/table_node.go
@@ -9,6 +9,7 @@
 	"source.monogon.dev/metropolis/node/core/identity"
 	apb "source.monogon.dev/metropolis/proto/api"
 	cpb "source.monogon.dev/metropolis/proto/common"
+	"source.monogon.dev/version"
 )
 
 func nodeEntry(n *apb.Node) clitable.Entry {
@@ -49,6 +50,10 @@
 	}
 	res.Add("tpm", tpm)
 
+	if n.Status.Version != nil {
+		res.Add("version", version.Semver(n.Status.Version))
+	}
+
 	tshs := n.TimeSinceHeartbeat.GetSeconds()
 	res.Add("heartbeat", fmt.Sprintf("%ds", tshs))
 
diff --git a/metropolis/cli/metroctl/test/BUILD.bazel b/metropolis/cli/metroctl/test/BUILD.bazel
index 382c73f..36f5a2d 100644
--- a/metropolis/cli/metroctl/test/BUILD.bazel
+++ b/metropolis/cli/metroctl/test/BUILD.bazel
@@ -1,5 +1,7 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
+# TODO(q3k): clean this up to just a go_test.
+
 go_test(
     name = "metroctl_test",
     srcs = ["test.go"],
@@ -16,6 +18,8 @@
         "//metropolis/pkg/cmd",
         "//metropolis/test/launch/cluster",
         "//metropolis/test/util",
+        "//metropolis/version",
+        "//version",
     ],
 )
 
@@ -29,5 +33,7 @@
         "//metropolis/pkg/cmd",
         "//metropolis/test/launch/cluster",
         "//metropolis/test/util",
+        "//metropolis/version",
+        "//version",
     ],
 )
diff --git a/metropolis/cli/metroctl/test/test.go b/metropolis/cli/metroctl/test/test.go
index 0145cfe..969b4cc 100644
--- a/metropolis/cli/metroctl/test/test.go
+++ b/metropolis/cli/metroctl/test/test.go
@@ -15,6 +15,8 @@
 	"source.monogon.dev/metropolis/pkg/cmd"
 	"source.monogon.dev/metropolis/test/launch/cluster"
 	"source.monogon.dev/metropolis/test/util"
+	mversion "source.monogon.dev/metropolis/version"
+	"source.monogon.dev/version"
 )
 
 // resolveMetroctl resolves metroctl filesystem path. It will return a correct
@@ -238,10 +240,10 @@
 			line := scanner.Text()
 			t.Logf("Line: %q", line)
 
-			var onid, ostate, onaddr, onstatus, onroles, ontpm string
+			var onid, ostate, onaddr, onstatus, onroles, ontpm, onver string
 			var ontimeout int
 
-			_, err = fmt.Sscanf(line, "%s%s%s%s%s%s%ds", &onid, &ostate, &onaddr, &onstatus, &onroles, &ontpm, &ontimeout)
+			_, err = fmt.Sscanf(line, "%s%s%s%s%s%s%s%ds", &onid, &ostate, &onaddr, &onstatus, &onroles, &ontpm, &onver, &ontimeout)
 			if err != nil {
 				return fmt.Errorf("while parsing metroctl output: %v", err)
 			}
@@ -263,6 +265,9 @@
 			if want, got := "yes", ontpm; want != got {
 				return fmt.Errorf("node tpm mismatch: wanted %q, got %q", want, got)
 			}
+			if want, got := version.Semver(mversion.Version), onver; want != got {
+				return fmt.Errorf("node version mismatch: wanted %q, got %q", want, got)
+			}
 			if ontimeout < 0 || ontimeout > 30 {
 				return fmt.Errorf("node timeout mismatch")
 			}
diff --git a/metropolis/node/core/roleserve/BUILD.bazel b/metropolis/node/core/roleserve/BUILD.bazel
index dd224d5..afad843 100644
--- a/metropolis/node/core/roleserve/BUILD.bazel
+++ b/metropolis/node/core/roleserve/BUILD.bazel
@@ -42,6 +42,7 @@
         "//metropolis/pkg/pki",
         "//metropolis/pkg/supervisor",
         "//metropolis/proto/common",
+        "//metropolis/version",
         "@org_golang_google_grpc//:go_default_library",
         "@org_golang_google_protobuf//encoding/prototext",
         "@org_golang_google_protobuf//proto",
@@ -62,6 +63,7 @@
         "//metropolis/pkg/supervisor",
         "//metropolis/proto/common",
         "//metropolis/test/util",
+        "//metropolis/version",
         "@com_github_cenkalti_backoff_v4//:backoff",
         "@com_github_google_go_cmp//cmp",
         "@org_golang_google_grpc//:go_default_library",
diff --git a/metropolis/node/core/roleserve/worker_statuspush.go b/metropolis/node/core/roleserve/worker_statuspush.go
index c27605f..e2a5cb9 100644
--- a/metropolis/node/core/roleserve/worker_statuspush.go
+++ b/metropolis/node/core/roleserve/worker_statuspush.go
@@ -11,6 +11,7 @@
 	"source.monogon.dev/metropolis/pkg/event"
 	"source.monogon.dev/metropolis/pkg/event/memory"
 	"source.monogon.dev/metropolis/pkg/supervisor"
+	"source.monogon.dev/metropolis/version"
 
 	ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	cpb "source.monogon.dev/metropolis/proto/common"
@@ -41,7 +42,9 @@
 // workerStatusPushLoop runs the main loop acting on data received from
 // workerStatusPushChannels.
 func workerStatusPushLoop(ctx context.Context, chans *workerStatusPushChannels) error {
-	status := cpb.NodeStatus{}
+	status := cpb.NodeStatus{
+		Version: version.Version,
+	}
 	var cur ipb.CuratorClient
 	var nodeID string
 
diff --git a/metropolis/node/core/roleserve/worker_statuspush_test.go b/metropolis/node/core/roleserve/worker_statuspush_test.go
index a237360..d1a8d2d 100644
--- a/metropolis/node/core/roleserve/worker_statuspush_test.go
+++ b/metropolis/node/core/roleserve/worker_statuspush_test.go
@@ -20,6 +20,7 @@
 	"source.monogon.dev/metropolis/node/core/curator"
 	"source.monogon.dev/metropolis/pkg/supervisor"
 	"source.monogon.dev/metropolis/test/util"
+	mversion "source.monogon.dev/metropolis/version"
 
 	ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	cpb "source.monogon.dev/metropolis/proto/common"
@@ -113,6 +114,7 @@
 	cur.expectReports(t, []*ipb.UpdateNodeStatusRequest{
 		{NodeId: nodeID, Status: &cpb.NodeStatus{
 			ExternalAddress: "192.0.2.10",
+			Version:         mversion.Version,
 		}},
 	})
 
@@ -122,9 +124,11 @@
 	cur.expectReports(t, []*ipb.UpdateNodeStatusRequest{
 		{NodeId: nodeID, Status: &cpb.NodeStatus{
 			ExternalAddress: "192.0.2.10",
+			Version:         mversion.Version,
 		}},
 		{NodeId: nodeID, Status: &cpb.NodeStatus{
 			ExternalAddress: "192.0.2.11",
+			Version:         mversion.Version,
 		}},
 	})
 
@@ -140,18 +144,22 @@
 	cur.expectReports(t, []*ipb.UpdateNodeStatusRequest{
 		{NodeId: nodeID, Status: &cpb.NodeStatus{
 			ExternalAddress: "192.0.2.10",
+			Version:         mversion.Version,
 		}},
 		{NodeId: nodeID, Status: &cpb.NodeStatus{
 			ExternalAddress: "192.0.2.11",
+			Version:         mversion.Version,
 		}},
 		{NodeId: nodeID, Status: &cpb.NodeStatus{
 			ExternalAddress: "192.0.2.11",
 			RunningCurator: &cpb.NodeStatus_RunningCurator{
 				Port: int32(common.CuratorServicePort),
 			},
+			Version: mversion.Version,
 		}},
 		{NodeId: nodeID, Status: &cpb.NodeStatus{
 			ExternalAddress: "192.0.2.11",
+			Version:         mversion.Version,
 		}},
 	})
 }
diff --git a/metropolis/proto/common/BUILD.bazel b/metropolis/proto/common/BUILD.bazel
index 5ddb3f7..05c4995 100644
--- a/metropolis/proto/common/BUILD.bazel
+++ b/metropolis/proto/common/BUILD.bazel
@@ -7,6 +7,7 @@
     srcs = ["common.proto"],
     visibility = ["//metropolis:__subpackages__"],
     deps = [
+        "//version/spec:spec_proto",
         "@com_google_protobuf//:timestamp_proto",
     ],
 )
@@ -16,6 +17,7 @@
     importpath = "source.monogon.dev/metropolis/proto/common",
     proto = ":common_proto",
     visibility = ["//metropolis:__subpackages__"],
+    deps = ["//version/spec"],
 )
 
 go_library(
diff --git a/metropolis/proto/common/common.proto b/metropolis/proto/common/common.proto
index d2b307e..fc461ab 100644
--- a/metropolis/proto/common/common.proto
+++ b/metropolis/proto/common/common.proto
@@ -19,6 +19,7 @@
 option go_package = "source.monogon.dev/metropolis/proto/common";
 
 import "google/protobuf/timestamp.proto";
+import "version/spec/spec.proto";
 
 // NodeRoles are the possible roles that a Metropolis Node should run within the
 // cluster. These are configured by the cluster and can be retrieved through the
@@ -149,6 +150,8 @@
     // timestamp is an epoch number associated with the last status update.
     // It's set with a nanosecond granularity.
     google.protobuf.Timestamp timestamp = 2;
+    // version is the Metropolis version that this node is running.
+    version.spec.Version version = 4;
 }
 
 // The Cluster Directory is information about the network addressing of nodes