metropolis/cli/metroctl: refactor to use RunE instead of log.Fatal

Change-Id: Id5ca65980816e1715a8f08afcdf712292117012a
Reviewed-on: https://review.monogon.dev/c/monogon/+/3441
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/cmd_node_set.go b/metropolis/cli/metroctl/cmd_node_set.go
index 59df43e..bdba8a9 100644
--- a/metropolis/cli/metroctl/cmd_node_set.go
+++ b/metropolis/cli/metroctl/cmd_node_set.go
@@ -2,6 +2,7 @@
 
 import (
 	"context"
+	"fmt"
 	"log"
 	"os"
 	"os/signal"
@@ -26,8 +27,45 @@
 	Short:   "Updates node roles.",
 	Use:     "role <KubernetesController|KubernetesWorker|ConsensusMember> [NodeID, ...]",
 	Example: "metroctl node add role KubernetesWorker metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
-	Args:    PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
-	Run:     doAdd,
+	Args:    PrintUsageOnWrongArgs(cobra.MinimumNArgs(2)),
+	RunE: func(cmd *cobra.Command, args []string) error {
+		ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
+		cc, err := dialAuthenticated(ctx)
+		if err != nil {
+			return fmt.Errorf("while dialing node: %w", err)
+		}
+		mgmt := api.NewManagementClient(cc)
+
+		role := strings.ToLower(args[0])
+		nodes := args[1:]
+
+		opt := func(v bool) *bool { return &v }
+		for _, node := range nodes {
+			req := &api.UpdateNodeRolesRequest{
+				Node: &api.UpdateNodeRolesRequest_Id{
+					Id: node,
+				},
+			}
+			switch role {
+			case "kubernetescontroller", "kc":
+				req.KubernetesController = opt(true)
+			case "kubernetesworker", "kw":
+				req.KubernetesWorker = opt(true)
+			case "consensusmember", "cm":
+				req.ConsensusMember = opt(true)
+			default:
+				return fmt.Errorf("unknown role: %s", role)
+			}
+
+			_, err := mgmt.UpdateNodeRoles(ctx, req)
+			if err != nil {
+				log.Printf("Couldn't update node \"%s\": %v", node, err)
+			} else {
+				log.Printf("Updated node %s.", node)
+			}
+		}
+		return nil
+	},
 }
 
 var removeRoleCmd = &cobra.Command{
@@ -35,7 +73,45 @@
 	Use:     "role <KubernetesController|KubernetesWorker|ConsensusMember> [NodeID, ...]",
 	Example: "metroctl node remove role KubernetesWorker metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
 	Args:    PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
-	Run:     doRemove,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
+		cc, err := dialAuthenticated(ctx)
+		if err != nil {
+			return fmt.Errorf("while dialing node: %w", err)
+		}
+		mgmt := api.NewManagementClient(cc)
+
+		role := strings.ToLower(args[0])
+		nodes := args[1:]
+
+		opt := func(v bool) *bool { return &v }
+		for _, node := range nodes {
+			req := &api.UpdateNodeRolesRequest{
+				Node: &api.UpdateNodeRolesRequest_Id{
+					Id: node,
+				},
+			}
+
+			switch role {
+			case "kubernetescontroller", "kc":
+				req.KubernetesController = opt(false)
+			case "kubernetesworker", "kw":
+				req.KubernetesWorker = opt(false)
+			case "consensusmember", "cm":
+				req.ConsensusMember = opt(false)
+			default:
+				return fmt.Errorf("unknown role: %s. Must be one of: KubernetesController, KubernetesWorker, ConsensusMember", role)
+			}
+
+			_, err := mgmt.UpdateNodeRoles(ctx, req)
+			if err != nil {
+				log.Printf("Couldn't update node \"%s\": %v", node, err)
+			} else {
+				log.Printf("Updated node %s.", node)
+			}
+		}
+		return nil
+	},
 }
 
 func init() {
@@ -45,82 +121,3 @@
 	removeCmd.AddCommand(removeRoleCmd)
 	nodeCmd.AddCommand(removeCmd)
 }
-
-func doAdd(cmd *cobra.Command, args []string) {
-	ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
-	cc := dialAuthenticated(ctx)
-	mgmt := api.NewManagementClient(cc)
-
-	if len(args) < 2 {
-		log.Fatal("Provide the role parameter together with at least one node ID.")
-	}
-
-	role := strings.ToLower(args[0])
-	nodes := args[1:]
-
-	opt := func(v bool) *bool { return &v }
-	for _, node := range nodes {
-		req := &api.UpdateNodeRolesRequest{
-			Node: &api.UpdateNodeRolesRequest_Id{
-				Id: node,
-			},
-		}
-		switch role {
-		case "kubernetescontroller", "kc":
-			req.KubernetesController = opt(true)
-		case "kubernetesworker", "kw":
-			req.KubernetesWorker = opt(true)
-		case "consensusmember", "cm":
-			req.ConsensusMember = opt(true)
-		default:
-			log.Fatalf("Unknown role: %s", role)
-		}
-
-		_, err := mgmt.UpdateNodeRoles(ctx, req)
-		if err != nil {
-			log.Printf("Couldn't update node \"%s\": %v", node, err)
-		} else {
-			log.Printf("Updated node %s.", node)
-		}
-	}
-}
-
-func doRemove(cmd *cobra.Command, args []string) {
-	ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
-	cc := dialAuthenticated(ctx)
-	mgmt := api.NewManagementClient(cc)
-
-	if len(args) < 2 {
-		log.Fatal("Provide the role parameter together with at least one node ID.")
-	}
-
-	role := strings.ToLower(args[0])
-	nodes := args[1:]
-
-	opt := func(v bool) *bool { return &v }
-	for _, node := range nodes {
-		req := &api.UpdateNodeRolesRequest{
-			Node: &api.UpdateNodeRolesRequest_Id{
-				Id: node,
-			},
-		}
-
-		switch role {
-		case "kubernetescontroller", "kc":
-			req.KubernetesController = opt(false)
-		case "kubernetesworker", "kw":
-			req.KubernetesWorker = opt(false)
-		case "consensusmember", "cm":
-			req.ConsensusMember = opt(false)
-		default:
-			log.Fatalf("Unknown role: %s. Must be one of: KubernetesController, KubernetesWorker, ConsensusMember.", role)
-		}
-
-		_, err := mgmt.UpdateNodeRoles(ctx, req)
-		if err != nil {
-			log.Printf("Couldn't update node \"%s\": %v", node, err)
-		} else {
-			log.Printf("Updated node %s.", node)
-		}
-	}
-}