m/c/metroctl: implement the approve command

This adds a command to approve new nodes, and list nodes pending
approval.

Change-Id: I8f91d6e549c1eae298c5a4a6f11b53ae70a77f79
Reviewed-on: https://review.monogon.dev/c/monogon/+/825
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 e8dc410..4ecdd60 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -3,6 +3,7 @@
 go_library(
     name = "metroctl_lib",
     srcs = [
+        "approve.go",
         "credentials.go",
         "install.go",
         "k8scredplugin.go",
@@ -23,6 +24,7 @@
         "//metropolis/cli/pkg/context",
         "//metropolis/cli/pkg/datafile",
         "//metropolis/node",
+        "//metropolis/node/core/identity",
         "//metropolis/node/core/rpc",
         "//metropolis/node/core/rpc/resolver",
         "//metropolis/proto/api",
diff --git a/metropolis/cli/metroctl/approve.go b/metropolis/cli/metroctl/approve.go
new file mode 100644
index 0000000..4a99e9d
--- /dev/null
+++ b/metropolis/cli/metroctl/approve.go
@@ -0,0 +1,110 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"log"
+
+	"github.com/spf13/cobra"
+
+	clicontext "source.monogon.dev/metropolis/cli/pkg/context"
+	"source.monogon.dev/metropolis/node/core/identity"
+	"source.monogon.dev/metropolis/proto/api"
+)
+
+var approveCmd = &cobra.Command{
+	Short: "Approves a candidate node, if specified; lists nodes pending approval otherwise.",
+	Use:   "approve [node-id]",
+	Args:  cobra.MaximumNArgs(1), // One positional argument: node ID
+	Run:   doApprove,
+}
+
+func init() {
+	rootCmd.AddCommand(approveCmd)
+}
+
+// getNewNodes returns all nodes pending approval within the cluster.
+func getNewNodes(ctx context.Context, mgmt api.ManagementClient) ([]*api.Node, error) {
+	resN, err := mgmt.GetNodes(ctx, &api.GetNodesRequest{
+		Filter: "node.state == NODE_STATE_NEW",
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	var nodes []*api.Node
+	for {
+		node, err := resN.Recv()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+		nodes = append(nodes, node)
+	}
+	return nodes, nil
+}
+
+// nodeById returns the node matching id, if it exists within nodes.
+func nodeById(nodes []*api.Node, id string) *api.Node {
+	for _, n := range nodes {
+		if identity.NodeID(n.Pubkey) == id {
+			return n
+		}
+	}
+	return nil
+}
+
+func doApprove(cmd *cobra.Command, args []string) {
+	// Collect credentials, validate command parameters, and try dialing the
+	// cluster.
+	ocert, opkey, err := getCredentials()
+	if err == noCredentialsError {
+		log.Fatalf("You have to take ownership of the cluster first: %v", err)
+	}
+	if len(flags.clusterEndpoints) == 0 {
+		log.Fatal("Please provide at least one cluster endpoint using the --endpoint parameter.")
+	}
+	ctx := clicontext.WithInterrupt(context.Background())
+	cc, err := dialCluster(ctx, opkey, ocert, "", flags.clusterEndpoints)
+	if err != nil {
+		log.Fatalf("While dialing the cluster: %v", err)
+	}
+	mgmt := api.NewManagementClient(cc)
+
+	// Get a list of all nodes pending approval by calling Management.GetNodes.
+	// We need this list regardless of whether we're actually approving nodes, or
+	// just listing them.
+	nodes, err := getNewNodes(ctx, mgmt)
+	if err != nil {
+		log.Fatalf("While fetching a list of nodes pending approval: %v", err)
+	}
+
+	if len(args) == 0 {
+		// If no id was given, just list the nodes pending approval.
+		if len(nodes) != 0 {
+			for _, n := range nodes {
+				fmt.Print(identity.NodeID(n.Pubkey))
+			}
+		} else {
+			log.Print("There are no nodes pending approval at this time.")
+		}
+	} else {
+		// Otherwise, try to approve the node matching the id.
+		tgtNodeId := args[0]
+
+		n := nodeById(nodes, tgtNodeId)
+		if n == nil {
+			log.Fatalf("Couldn't find a new node matching id %s", tgtNodeId)
+		}
+		_, err := mgmt.ApproveNode(ctx, &api.ApproveNodeRequest{
+			Pubkey: n.Pubkey,
+		})
+		if err != nil {
+			log.Fatalf("While approving node %s: %v", tgtNodeId, err)
+		}
+		log.Printf("Approved node %s.", tgtNodeId)
+	}
+}