m/n/core/curator: implement leader election

This implements the leader election functionality subset of the curator.
It does not yet implement any business logic, just the switchover
between acting as a leader and a follower.

Test plan: implements an integration test for the leader election with
an in-memory etcd cluster.

Change-Id: Id77ecc35a9f2b18e716fffd3caf2de193982d676
Reviewed-on: https://review.monogon.dev/c/monogon/+/184
Reviewed-by: Lorenz Brun <lorenz@nexantic.com>
diff --git a/metropolis/node/core/main.go b/metropolis/node/core/main.go
index eb4c6c7..d9f408e 100644
--- a/metropolis/node/core/main.go
+++ b/metropolis/node/core/main.go
@@ -28,12 +28,14 @@
 	"os"
 	"os/signal"
 	"runtime/debug"
+	"time"
 
 	"golang.org/x/sys/unix"
 	"google.golang.org/grpc"
 
 	common "source.monogon.dev/metropolis/node"
 	"source.monogon.dev/metropolis/node/core/cluster"
+	"source.monogon.dev/metropolis/node/core/curator"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 	"source.monogon.dev/metropolis/node/core/localstorage/declarative"
 	"source.monogon.dev/metropolis/node/core/network"
@@ -165,6 +167,22 @@
 			return fmt.Errorf("new couldn't find home in new cluster, aborting: %w", err)
 		}
 
+		// Start cluster curator.
+		kv, err := status.ConsensusClient(cluster.ConsensusUserCurator)
+		if err != nil {
+			return fmt.Errorf("failed to retrieve consensus curator client: %w", err)
+		}
+		c := curator.New(curator.Config{
+			Etcd:   kv,
+			NodeID: status.Node.ID(),
+			// TODO(q3k): make this configurable?
+			LeaderTTL: time.Second * 5,
+			Directory: &root.Ephemeral.Curator,
+		})
+		if err := supervisor.Run(ctx, "curator", c.Run); err != nil {
+			return fmt.Errorf("when starting curator: %w", err)
+		}
+
 		// We are now in a cluster. We can thus access our 'node' object and
 		// start all services that we should be running.