metropolis/cli/metroctl: don't print usage for runtime errors

Change-Id: I91fdca45e874ea1ec9112df556a9a7392fd45bfd
Fixes: https://github.com/monogon-dev/monogon/issues/352
Reviewed-on: https://review.monogon.dev/c/monogon/+/3440
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/cmd_certs.go b/metropolis/cli/metroctl/cmd_certs.go
index d914c83..8475546 100644
--- a/metropolis/cli/metroctl/cmd_certs.go
+++ b/metropolis/cli/metroctl/cmd_certs.go
@@ -48,5 +48,5 @@
 		}
 		log.Println("Wrote files to current dir: cert.pem, key.pem")
 	},
-	Args: cobra.NoArgs,
+	Args: PrintUsageOnWrongArgs(cobra.NoArgs),
 }
diff --git a/metropolis/cli/metroctl/cmd_install_usb.go b/metropolis/cli/metroctl/cmd_install_usb.go
index a9873b6..fcbcfa2 100644
--- a/metropolis/cli/metroctl/cmd_install_usb.go
+++ b/metropolis/cli/metroctl/cmd_install_usb.go
@@ -13,7 +13,7 @@
 	Use:     "genusb target",
 	Short:   "Generates a Metropolis installer disk or image.",
 	Example: "metroctl install --bundle=metropolis-v0.1.zip genusb /dev/sdx",
-	Args:    cobra.ExactArgs(1), // One positional argument: the target
+	Args:    PrintUsageOnWrongArgs(cobra.ExactArgs(1)), // One positional argument: the target
 	Run:     doGenUSB,
 }
 
diff --git a/metropolis/cli/metroctl/cmd_k8s_configure.go b/metropolis/cli/metroctl/cmd_k8s_configure.go
index c238cdf..cd90416 100644
--- a/metropolis/cli/metroctl/cmd_k8s_configure.go
+++ b/metropolis/cli/metroctl/cmd_k8s_configure.go
@@ -23,7 +23,7 @@
 	Long: `Configures a local kubectl instance (or any other Kubernetes application)
 to connect to a Metropolis cluster. A cluster endpoint must be provided with the
 --endpoints parameter.`,
-	Args: cobra.ExactArgs(0),
+	Args: PrintUsageOnWrongArgs(cobra.ExactArgs(0)),
 	Run:  doK8sConfigure,
 }
 
diff --git a/metropolis/cli/metroctl/cmd_k8scredplugin.go b/metropolis/cli/metroctl/cmd_k8scredplugin.go
index 0d1f318..8d84a9e 100644
--- a/metropolis/cli/metroctl/cmd_k8scredplugin.go
+++ b/metropolis/cli/metroctl/cmd_k8scredplugin.go
@@ -21,7 +21,7 @@
 	Long: `This implements a Kubernetes client-go credential plugin to
 authenticate client-go based callers including kubectl against a Metropolis
 cluster. This should never be directly called by end users.`,
-	Args:   cobra.ExactArgs(0),
+	Args:   PrintUsageOnWrongArgs(cobra.ExactArgs(0)),
 	Hidden: true,
 	Run:    doK8sCredPlugin,
 }
diff --git a/metropolis/cli/metroctl/cmd_node.go b/metropolis/cli/metroctl/cmd_node.go
index 59b4d9b..d7a23ee 100644
--- a/metropolis/cli/metroctl/cmd_node.go
+++ b/metropolis/cli/metroctl/cmd_node.go
@@ -53,7 +53,7 @@
 		}
 		printNodes(nodes, args, columns)
 	},
-	Args: cobra.ArbitraryArgs,
+	Args: PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
 }
 
 var nodeListCmd = &cobra.Command{
@@ -72,7 +72,7 @@
 
 		printNodes(nodes, args, map[string]bool{"node id": true})
 	},
-	Args: cobra.ArbitraryArgs,
+	Args: PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
 }
 
 var nodeUpdateCmd = &cobra.Command{
@@ -225,7 +225,7 @@
 
 		return nil
 	},
-	Args: cobra.MinimumNArgs(1),
+	Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(1)),
 }
 
 var nodeDeleteCmd = &cobra.Command{
@@ -283,7 +283,7 @@
 		_, err = mgmt.DeleteNode(ctx, req)
 		return err
 	},
-	Args: cobra.ExactArgs(1),
+	Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
 }
 
 func dialNode(ctx context.Context, node string) (apb.NodeManagementClient, error) {
@@ -331,7 +331,7 @@
 --firmware flag. This flag cannot be combined with any others.
 	`,
 	Use:          "reboot [node-id]",
-	Args:         cobra.ExactArgs(1),
+	Args:         PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
 	SilenceUsage: true,
 	RunE: func(cmd *cobra.Command, args []string) error {
 		ctx := cmd.Context()
@@ -385,7 +385,7 @@
 var nodePoweroffCmd = &cobra.Command{
 	Short:        "Power off a node",
 	Use:          "poweroff [node-id]",
-	Args:         cobra.ExactArgs(1),
+	Args:         PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
 	SilenceUsage: true,
 	RunE: func(cmd *cobra.Command, args []string) error {
 		ctx := cmd.Context()
diff --git a/metropolis/cli/metroctl/cmd_node_approve.go b/metropolis/cli/metroctl/cmd_node_approve.go
index 62ab7bd..294f8c3 100644
--- a/metropolis/cli/metroctl/cmd_node_approve.go
+++ b/metropolis/cli/metroctl/cmd_node_approve.go
@@ -17,7 +17,7 @@
 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
+	Args:  PrintUsageOnWrongArgs(cobra.MaximumNArgs(1)), // One positional argument: node ID
 	Run:   doApprove,
 }
 
diff --git a/metropolis/cli/metroctl/cmd_node_logs.go b/metropolis/cli/metroctl/cmd_node_logs.go
index 5b8cef9..d21df7b 100644
--- a/metropolis/cli/metroctl/cmd_node_logs.go
+++ b/metropolis/cli/metroctl/cmd_node_logs.go
@@ -54,7 +54,7 @@
 unnecessary lines are fetched.
 `,
 	Use:  "logs [node-id]",
-	Args: cobra.MinimumNArgs(1),
+	Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(1)),
 	RunE: func(cmd *cobra.Command, args []string) error {
 		ctx := cmd.Context()
 
diff --git a/metropolis/cli/metroctl/cmd_node_metrics.go b/metropolis/cli/metroctl/cmd_node_metrics.go
index f3bde46..0842c24 100644
--- a/metropolis/cli/metroctl/cmd_node_metrics.go
+++ b/metropolis/cli/metroctl/cmd_node_metrics.go
@@ -39,7 +39,7 @@
 
 `,
 	Use:  "metrics [node-id] [exporter]",
-	Args: cobra.MinimumNArgs(2),
+	Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(2)),
 	RunE: func(cmd *cobra.Command, args []string) error {
 		ctx := cmd.Context()
 
diff --git a/metropolis/cli/metroctl/cmd_node_set.go b/metropolis/cli/metroctl/cmd_node_set.go
index 73d7f3c..59df43e 100644
--- a/metropolis/cli/metroctl/cmd_node_set.go
+++ b/metropolis/cli/metroctl/cmd_node_set.go
@@ -26,7 +26,7 @@
 	Short:   "Updates node roles.",
 	Use:     "role <KubernetesController|KubernetesWorker|ConsensusMember> [NodeID, ...]",
 	Example: "metroctl node add role KubernetesWorker metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
-	Args:    cobra.ArbitraryArgs,
+	Args:    PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
 	Run:     doAdd,
 }
 
@@ -34,7 +34,7 @@
 	Short:   "Updates node roles.",
 	Use:     "role <KubernetesController|KubernetesWorker|ConsensusMember> [NodeID, ...]",
 	Example: "metroctl node remove role KubernetesWorker metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
-	Args:    cobra.ArbitraryArgs,
+	Args:    PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
 	Run:     doRemove,
 }
 
diff --git a/metropolis/cli/metroctl/cmd_takeownership.go b/metropolis/cli/metroctl/cmd_takeownership.go
index 30b9f08..d421f71 100644
--- a/metropolis/cli/metroctl/cmd_takeownership.go
+++ b/metropolis/cli/metroctl/cmd_takeownership.go
@@ -24,7 +24,7 @@
 cluster to issue an owner certificate to for the owner key generated by a
 previous invocation of metroctl install on this machine. A single cluster
 endpoint must be provided with the --endpoints parameter.`,
-	Args: cobra.ExactArgs(0),
+	Args: PrintUsageOnWrongArgs(cobra.ExactArgs(0)),
 	Run:  doTakeOwnership,
 }
 
diff --git a/metropolis/cli/metroctl/main.go b/metropolis/cli/metroctl/main.go
index fcdbccd..21f87a6 100644
--- a/metropolis/cli/metroctl/main.go
+++ b/metropolis/cli/metroctl/main.go
@@ -15,8 +15,10 @@
 
 // rootCmd represents the base command when called without any subcommands
 var rootCmd = &cobra.Command{
-	Use:   "metroctl",
-	Short: "metroctl controls Metropolis nodes and clusters.",
+	Use:           "metroctl",
+	Short:         "metroctl controls Metropolis nodes and clusters.",
+	SilenceUsage:  true,
+	SilenceErrors: true,
 }
 
 type metroctlFlags struct {
@@ -60,6 +62,20 @@
 	rootCmd.PersistentFlags().StringVar(&flags.columns, "columns", "", "Comma-separated list of column names to show. If not set, all columns will be shown")
 	rootCmd.PersistentFlags().StringVarP(&flags.output, "output", "o", "", "Redirects output to the specified file")
 	rootCmd.PersistentFlags().BoolVar(&flags.acceptAnyCA, "insecure-accept-and-persist-first-encountered-ca", false, "Accept the first encountered CA while connecting as the trusted CA for future metroctl connections with this config path. This is very insecure and should only be used for testing.")
+	rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
+		cmd.PrintErr(cmd.UsageString())
+		return err
+	})
+}
+
+func PrintUsageOnWrongArgs(pArgs cobra.PositionalArgs) cobra.PositionalArgs {
+	return func(cmd *cobra.Command, args []string) error {
+		err := pArgs(cmd, args)
+		if err != nil {
+			cmd.PrintErr(cmd.UsageString())
+		}
+		return err
+	}
 }
 
 func main() {