m/c/metroctl: add takeownership command

This add a simple command to take ownership of a cluster previously
installed using metroctl install. It calls the newly-formed cluster and
retrieves a signed owner certificate for the owner key and stores that
to disk for further use by metroctl.

Change-Id: Ibd2771c571bda41270c3bbb110105f4f8f5b118d
Reviewed-on: https://review.monogon.dev/c/monogon/+/463
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index eb09e96..6a311c8 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -5,6 +5,7 @@
     srcs = [
         "install.go",
         "main.go",
+        "takeownership.go",
     ],
     data = [
         "//metropolis/node",
@@ -14,6 +15,9 @@
     visibility = ["//visibility:private"],
     deps = [
         "//metropolis/cli/metroctl/core:go_default_library",
+        "//metropolis/cli/pkg/context:go_default_library",
+        "//metropolis/node:go_default_library",
+        "//metropolis/node/core/rpc:go_default_library",
         "//metropolis/proto/api:go_default_library",
         "@com_github_adrg_xdg//:go_default_library",
         "@com_github_spf13_cobra//:go_default_library",
diff --git a/metropolis/cli/metroctl/takeownership.go b/metropolis/cli/metroctl/takeownership.go
new file mode 100644
index 0000000..a264644
--- /dev/null
+++ b/metropolis/cli/metroctl/takeownership.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+	"context"
+	"crypto/ed25519"
+	"encoding/pem"
+	"log"
+	"net"
+	"os"
+	"path/filepath"
+
+	"github.com/adrg/xdg"
+	"github.com/spf13/cobra"
+
+	apb "source.monogon.dev/metropolis/proto/api"
+
+	clicontext "source.monogon.dev/metropolis/cli/pkg/context"
+	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/core/rpc"
+)
+
+var takeownershipCommand = &cobra.Command{
+	Use:   "takeownership <node-addr>",
+	Short: "Takes ownership of a new Metropolis cluster",
+	Long: `This takes ownership of a new Metropolis cluster by asking the new
+cluster to issue an owner certificate to for the owner key generated by a
+previous invocation of metroctl install on this machine.`,
+	Example: "takeownership 192.0.2.1",
+	Args:    cobra.ExactArgs(1), // One positional argument: the node address
+	Run:     doTakeOwnership,
+}
+
+func doTakeOwnership(cmd *cobra.Command, args []string) {
+	ctx := clicontext.WithInterrupt(context.Background())
+	ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
+	if os.IsNotExist(err) {
+		log.Fatalf("Owner key does not exist. takeownership needs to be executed on the same system that has previously installed the cluster using metroctl install.")
+	} else if err != nil {
+		log.Fatalf("Failed to load owner private key: %v", err)
+	}
+	block, _ := pem.Decode(ownerPrivateKeyPEM)
+	if block == nil {
+		log.Fatalf("owner-key.pem contains invalid PEM")
+	}
+	if block.Type != ownerKeyType {
+		log.Fatalf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
+	}
+	if len(block.Bytes) != ed25519.PrivateKeySize {
+		log.Fatal("owner-key.pem contains non-Ed25519 key")
+	}
+	ownerPrivateKey := ed25519.PrivateKey(block.Bytes)
+
+	client, err := rpc.NewEphemeralClient(net.JoinHostPort(args[0], node.CuratorServicePort.PortString()), ownerPrivateKey, nil)
+	if err != nil {
+		log.Fatalf("Failed to create client to given node address: %v", err)
+	}
+	defer client.Close()
+	aaa := apb.NewAAAClient(client)
+	ownerCert, err := rpc.RetrieveOwnerCertificate(ctx, aaa, ownerPrivateKey)
+	if err != nil {
+		log.Fatalf("Failed to retrive owner certificate from cluster: %v", err)
+	}
+	ownerCertPEM := pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: ownerCert.Certificate[0],
+	}
+	if err := os.WriteFile(filepath.Join(xdg.ConfigHome, "metroctl/owner.pem"), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
+		log.Printf("Failed to store retrieved owner certificate: %v", err)
+		log.Fatalln("Sorry, the cluster has been lost as taking ownership cannot be repeated. Fix the reason the file couldn't be written and reinstall the node.")
+	}
+	log.Print("Successfully retrieved owner credentials! You now own this cluster.")
+}
+
+func init() {
+	rootCmd.AddCommand(takeownershipCommand)
+}