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)
+ }
+}