metropolis/cli/metroctl: add `node delete` command

Change-Id: I804829efe3e78a56cd44915c8351272ffa5c670f
Reviewed-on: https://review.monogon.dev/c/monogon/+/2276
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/cmd_node.go b/metropolis/cli/metroctl/cmd_node.go
index 1e4afa5..a2946bf 100644
--- a/metropolis/cli/metroctl/cmd_node.go
+++ b/metropolis/cli/metroctl/cmd_node.go
@@ -146,13 +146,71 @@
 	Args: cobra.ExactArgs(1),
 }
 
+var nodeDeleteCmd = &cobra.Command{
+	Short:   "Deletes a node from the cluster.",
+	Use:     "delete [NodeID] [--bypass-has-roles] [--bypass-not-decommissioned]",
+	Example: "metroctl node delete metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		bypassHasRoles, err := cmd.Flags().GetBool("bypass-has-roles")
+		if err != nil {
+			return err
+		}
+
+		bypassNotDecommissioned, err := cmd.Flags().GetBool("bypass-not-decommissioned")
+		if err != nil {
+			return err
+		}
+
+		ctx := clicontext.WithInterrupt(context.Background())
+		mgmt := apb.NewManagementClient(dialAuthenticated(ctx))
+
+		nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id==%q", args[0]))
+		if err != nil {
+			return fmt.Errorf("while calling Management.GetNodes: %v", err)
+		}
+
+		if len(nodes) == 0 {
+			return fmt.Errorf("could not find node with id: %s", args[0])
+		}
+
+		if len(nodes) != 1 {
+			return fmt.Errorf("expected one node, got %d", len(nodes))
+		}
+
+		n := nodes[0]
+		log.Printf("deleting node: %s (%s)", n.Id, n.Status.ExternalAddress)
+
+		req := &apb.DeleteNodeRequest{
+			Node: &apb.DeleteNodeRequest_Id{
+				Id: n.Id,
+			},
+		}
+
+		if bypassHasRoles {
+			req.SafetyBypassHasRoles = &apb.DeleteNodeRequest_SafetyBypassHasRoles{}
+		}
+
+		if bypassNotDecommissioned {
+			req.SafetyBypassNotDecommissioned = &apb.DeleteNodeRequest_SafetyBypassNotDecommissioned{}
+		}
+
+		_, err = mgmt.DeleteNode(ctx, req)
+		return err
+	},
+	Args: cobra.ExactArgs(1),
+}
+
 func init() {
 	nodeUpdateCmd.Flags().String("bundle-url", "", "The URL to the new version")
 	nodeUpdateCmd.Flags().String("activation-mode", "reboot", "How the update should be activated (kexec, reboot, none)")
 
+	nodeDeleteCmd.Flags().Bool("bypass-has-roles", false, "Allows to bypass the HasRoles check")
+	nodeDeleteCmd.Flags().Bool("bypass-not-decommissioned", false, "Allows to bypass the NotDecommissioned check")
+
 	nodeCmd.AddCommand(nodeDescribeCmd)
 	nodeCmd.AddCommand(nodeListCmd)
 	nodeCmd.AddCommand(nodeUpdateCmd)
+	nodeCmd.AddCommand(nodeDeleteCmd)
 	rootCmd.AddCommand(nodeCmd)
 }