From 9e6919550be5689628d0020ec14e90ea6f527716 Mon Sep 17 00:00:00 2001
From: Francis Lavoie <lavofr@gmail.com>
Date: Fri, 24 Feb 2023 18:09:12 -0500
Subject: cmd: Expand cobra support, add short flags (#5379)

* cmd: Expand cobra support

* Convert commands to cobra, add short flags

* Fix version command typo

Co-authored-by: Emily Lange <git@indeednotjames.com>

* Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

---------

Co-authored-by: Emily Lange <git@indeednotjames.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
---
 modules/caddyhttp/caddyauth/command.go    | 16 +++++------
 modules/caddyhttp/fileserver/command.go   | 24 ++++++++--------
 modules/caddyhttp/reverseproxy/command.go | 46 +++++++++++++++++--------------
 modules/caddyhttp/staticresp.go           | 31 ++++++++++-----------
 modules/caddypki/command.go               | 36 +++++++++++-------------
 5 files changed, 74 insertions(+), 79 deletions(-)

(limited to 'modules')

diff --git a/modules/caddyhttp/caddyauth/command.go b/modules/caddyhttp/caddyauth/command.go
index 609de4e..915bcd3 100644
--- a/modules/caddyhttp/caddyauth/command.go
+++ b/modules/caddyhttp/caddyauth/command.go
@@ -18,20 +18,19 @@ import (
 	"bufio"
 	"bytes"
 	"encoding/base64"
-	"flag"
 	"fmt"
 	"os"
 	"os/signal"
 
 	"github.com/caddyserver/caddy/v2"
 	caddycmd "github.com/caddyserver/caddy/v2/cmd"
+	"github.com/spf13/cobra"
 	"golang.org/x/term"
 )
 
 func init() {
 	caddycmd.RegisterCommand(caddycmd.Command{
 		Name:  "hash-password",
-		Func:  cmdHashPassword,
 		Usage: "[--algorithm <name>] [--salt <string>] [--plaintext <password>]",
 		Short: "Hashes a password and writes base64",
 		Long: `
@@ -50,13 +49,12 @@ be provided (scrypt).
 
 Note that scrypt is deprecated. Please use 'bcrypt' instead.
 `,
-		Flags: func() *flag.FlagSet {
-			fs := flag.NewFlagSet("hash-password", flag.ExitOnError)
-			fs.String("algorithm", "bcrypt", "Name of the hash algorithm")
-			fs.String("plaintext", "", "The plaintext password")
-			fs.String("salt", "", "The password salt")
-			return fs
-		}(),
+		CobraFunc: func(cmd *cobra.Command) {
+			cmd.Flags().StringP("plaintext", "p", "", "The plaintext password")
+			cmd.Flags().StringP("salt", "s", "", "The password salt")
+			cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm")
+			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
+		},
 	})
 }
 
diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go
index bc7f981..697ec34 100644
--- a/modules/caddyhttp/fileserver/command.go
+++ b/modules/caddyhttp/fileserver/command.go
@@ -16,7 +16,6 @@ package fileserver
 
 import (
 	"encoding/json"
-	"flag"
 	"log"
 	"strconv"
 	"time"
@@ -27,13 +26,13 @@ import (
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 	caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
 	"github.com/caddyserver/certmagic"
+	"github.com/spf13/cobra"
 	"go.uber.org/zap"
 )
 
 func init() {
 	caddycmd.RegisterCommand(caddycmd.Command{
 		Name:  "file-server",
-		Func:  cmdFileServer,
 		Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log]",
 		Short: "Spins up a production-ready file server",
 		Long: `
@@ -49,17 +48,16 @@ using this option.
 
 If --browse is enabled, requests for folders without an index file will
 respond with a file listing.`,
-		Flags: func() *flag.FlagSet {
-			fs := flag.NewFlagSet("file-server", flag.ExitOnError)
-			fs.String("domain", "", "Domain name at which to serve the files")
-			fs.String("root", "", "The path to the root of the site")
-			fs.String("listen", "", "The address to which to bind the listener")
-			fs.Bool("browse", false, "Enable directory browsing")
-			fs.Bool("templates", false, "Enable template rendering")
-			fs.Bool("access-log", false, "Enable the access log")
-			fs.Bool("debug", false, "Enable verbose debug logs")
-			return fs
-		}(),
+		CobraFunc: func(cmd *cobra.Command) {
+			cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
+			cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
+			cmd.Flags().StringP("listen", "", "", "The address to which to bind the listener")
+			cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing")
+			cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
+			cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
+			cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
+			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
+		},
 	})
 }
 
diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go
index 04fb9b4..8c171ec 100644
--- a/modules/caddyhttp/reverseproxy/command.go
+++ b/modules/caddyhttp/reverseproxy/command.go
@@ -16,7 +16,6 @@ package reverseproxy
 
 import (
 	"encoding/json"
-	"flag"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -28,14 +27,14 @@ import (
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
 	"github.com/caddyserver/caddy/v2/modules/caddytls"
+	"github.com/spf13/cobra"
 	"go.uber.org/zap"
 )
 
 func init() {
 	caddycmd.RegisterCommand(caddycmd.Command{
 		Name:  "reverse-proxy",
-		Func:  cmdReverseProxy,
-		Usage: "[--from <addr>] [--to <addr>] [--change-host-header] [--insecure] [--internal-certs] [--disable-redirects]",
+		Usage: "[--from <addr>] [--to <addr>] [--change-host-header] [--insecure] [--internal-certs] [--disable-redirects] [--access-log]",
 		Short: "A quick and production-ready reverse proxy",
 		Long: `
 A simple but production-ready reverse proxy. Useful for quick deployments,
@@ -63,17 +62,17 @@ For proxying:
   --insecure disables TLS verification with the upstream. WARNING: THIS
     DISABLES SECURITY BY NOT VERIFYING THE UPSTREAM'S CERTIFICATE.
 `,
-		Flags: func() *flag.FlagSet {
-			fs := flag.NewFlagSet("reverse-proxy", flag.ExitOnError)
-			fs.String("from", "localhost", "Address on which to receive traffic")
-			fs.Var(&reverseProxyCmdTo, "to", "Upstream address(es) to which traffic should be sent")
-			fs.Bool("change-host-header", false, "Set upstream Host header to address of upstream")
-			fs.Bool("insecure", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING TLS CERTIFICATES!)")
-			fs.Bool("internal-certs", false, "Use internal CA for issuing certs")
-			fs.Bool("debug", false, "Enable verbose debug logs")
-			fs.Bool("disable-redirects", false, "Disable HTTP->HTTPS redirects")
-			return fs
-		}(),
+		CobraFunc: func(cmd *cobra.Command) {
+			cmd.Flags().StringP("from", "f", "localhost", "Address on which to receive traffic")
+			cmd.Flags().StringSliceP("to", "t", []string{}, "Upstream address(es) to which traffic should be sent")
+			cmd.Flags().BoolP("change-host-header", "c", false, "Set upstream Host header to address of upstream")
+			cmd.Flags().BoolP("insecure", "", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING TLS CERTIFICATES!)")
+			cmd.Flags().BoolP("disable-redirects", "r", false, "Disable HTTP->HTTPS redirects")
+			cmd.Flags().BoolP("internal-certs", "i", false, "Use internal CA for issuing certs")
+			cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
+			cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
+			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdReverseProxy)
+		},
 	})
 }
 
@@ -83,14 +82,19 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
 	from := fs.String("from")
 	changeHost := fs.Bool("change-host-header")
 	insecure := fs.Bool("insecure")
+	disableRedir := fs.Bool("disable-redirects")
 	internalCerts := fs.Bool("internal-certs")
+	accessLog := fs.Bool("access-log")
 	debug := fs.Bool("debug")
-	disableRedir := fs.Bool("disable-redirects")
 
 	httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
 	httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
 
-	if len(reverseProxyCmdTo) == 0 {
+	to, err := fs.GetStringSlice("to")
+	if err != nil {
+		return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid to flag: %v", err)
+	}
+	if len(to) == 0 {
 		return caddy.ExitCodeFailedStartup, fmt.Errorf("--to is required")
 	}
 
@@ -119,9 +123,9 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
 
 	// set up the upstream address; assume missing information from given parts
 	// mixing schemes isn't supported, so use first defined (if available)
-	toAddresses := make([]string, len(reverseProxyCmdTo))
+	toAddresses := make([]string, len(to))
 	var toScheme string
-	for i, toLoc := range reverseProxyCmdTo {
+	for i, toLoc := range to {
 		addr, scheme, err := parseUpstreamDialAddress(toLoc)
 		if err != nil {
 			return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err)
@@ -180,6 +184,9 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
 		Routes: caddyhttp.RouteList{route},
 		Listen: []string{":" + fromAddr.Port},
 	}
+	if accessLog {
+		server.Logs = &caddyhttp.ServerLogConfig{}
+	}
 
 	if fromAddr.Scheme == "http" {
 		server.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{Disabled: true}
@@ -238,6 +245,3 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
 
 	select {}
 }
-
-// reverseProxyCmdTo holds the parsed values from repeated use of the --to flag.
-var reverseProxyCmdTo caddycmd.StringSlice
diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go
index add5b12..67614c2 100644
--- a/modules/caddyhttp/staticresp.go
+++ b/modules/caddyhttp/staticresp.go
@@ -17,7 +17,6 @@ package caddyhttp
 import (
 	"bytes"
 	"encoding/json"
-	"flag"
 	"fmt"
 	"io"
 	"net/http"
@@ -32,6 +31,7 @@ import (
 	"github.com/caddyserver/caddy/v2/caddyconfig"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 	caddycmd "github.com/caddyserver/caddy/v2/cmd"
+	"github.com/spf13/cobra"
 	"go.uber.org/zap"
 )
 
@@ -39,7 +39,6 @@ func init() {
 	caddy.RegisterModule(StaticResponse{})
 	caddycmd.RegisterCommand(caddycmd.Command{
 		Name:  "respond",
-		Func:  cmdRespond,
 		Usage: `[--status <code>] [--body <content>] [--listen <addr>] [--access-log] [--debug] [--header "Field: value"] <body|status>`,
 		Short: "Simple, hard-coded HTTP responses for development and testing",
 		Long: `
@@ -71,16 +70,15 @@ Access/request logging and more verbose debug logging can also be enabled.
 
 Response headers may be added using the --header flag for each header field.
 `,
-		Flags: func() *flag.FlagSet {
-			fs := flag.NewFlagSet("respond", flag.ExitOnError)
-			fs.String("listen", ":0", "The address to which to bind the listener")
-			fs.Int("status", http.StatusOK, "The response status code")
-			fs.String("body", "", "The body of the HTTP response")
-			fs.Bool("access-log", false, "Enable the access log")
-			fs.Bool("debug", false, "Enable more verbose debug-level logging")
-			fs.Var(&respondCmdHeaders, "header", "Set a header on the response (format: \"Field: value\"")
-			return fs
-		}(),
+		CobraFunc: func(cmd *cobra.Command) {
+			cmd.Flags().StringP("listen", "l", ":0", "The address to which to bind the listener")
+			cmd.Flags().IntP("status", "s", http.StatusOK, "The response status code")
+			cmd.Flags().StringP("body", "b", "", "The body of the HTTP response")
+			cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
+			cmd.Flags().BoolP("debug", "v", false, "Enable more verbose debug-level logging")
+			cmd.Flags().StringSliceP("header", "H", []string{}, "Set a header on the response (format: \"Field: value\")")
+			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdRespond)
+		},
 	})
 }
 
@@ -318,8 +316,12 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
 	}
 
 	// build headers map
+	headers, err := fl.GetStringSlice("header")
+	if err != nil {
+		return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
+	}
 	hdr := make(http.Header)
-	for i, h := range respondCmdHeaders {
+	for i, h := range headers {
 		key, val, found := strings.Cut(h, ":")
 		key, val = strings.TrimSpace(key), strings.TrimSpace(val)
 		if !found || key == "" || val == "" {
@@ -432,9 +434,6 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
 	select {}
 }
 
-// respondCmdHeaders holds the parsed values from repeated use of the --header flag.
-var respondCmdHeaders caddycmd.StringSlice
-
 // Interface guards
 var (
 	_ MiddlewareHandler     = (*StaticResponse)(nil)
diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go
index cb86c93..e78f35c 100644
--- a/modules/caddypki/command.go
+++ b/modules/caddypki/command.go
@@ -18,7 +18,6 @@ import (
 	"crypto/x509"
 	"encoding/json"
 	"encoding/pem"
-	"flag"
 	"fmt"
 	"net/http"
 	"os"
@@ -27,12 +26,12 @@ import (
 	"github.com/caddyserver/caddy/v2"
 	caddycmd "github.com/caddyserver/caddy/v2/cmd"
 	"github.com/smallstep/truststore"
+	"github.com/spf13/cobra"
 )
 
 func init() {
 	caddycmd.RegisterCommand(caddycmd.Command{
 		Name:  "trust",
-		Func:  cmdTrust,
 		Usage: "[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]",
 		Short: "Installs a CA certificate into local trust stores",
 		Long: `
@@ -53,19 +52,17 @@ This command will attempt to connect to Caddy's admin API running at
 '` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may
 explicitly specify the --address, or use the --config flag to load
 the admin address from your config, if not using the default.`,
-		Flags: func() *flag.FlagSet {
-			fs := flag.NewFlagSet("trust", flag.ExitOnError)
-			fs.String("ca", "", "The ID of the CA to trust (defaults to 'local')")
-			fs.String("address", "", "Address of the administration API listener (if --config is not used)")
-			fs.String("config", "", "Configuration file (if --address is not used)")
-			fs.String("adapter", "", "Name of config adapter to apply (if --config is used)")
-			return fs
-		}(),
+		CobraFunc: func(cmd *cobra.Command) {
+			cmd.Flags().StringP("ca", "", "", "The ID of the CA to trust (defaults to 'local')")
+			cmd.Flags().StringP("address", "", "", "Address of the administration API listener (if --config is not used)")
+			cmd.Flags().StringP("config", "c", "", "Configuration file (if --address is not used)")
+			cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (if --config is used)")
+			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdTrust)
+		},
 	})
 
 	caddycmd.RegisterCommand(caddycmd.Command{
 		Name:  "untrust",
-		Func:  cmdUntrust,
 		Usage: "[--cert <path>] | [[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]]",
 		Short: "Untrusts a locally-trusted CA certificate",
 		Long: `
@@ -89,15 +86,14 @@ will attempt to connect to the Caddy's admin API running at
 '` + caddy.DefaultAdminListen + `' to fetch the root certificate.
 You may explicitly specify the --address, or use the --config flag
 to load the admin address from your config, if not using the default.`,
-		Flags: func() *flag.FlagSet {
-			fs := flag.NewFlagSet("untrust", flag.ExitOnError)
-			fs.String("cert", "", "The path to the CA certificate to untrust")
-			fs.String("ca", "", "The ID of the CA to untrust (defaults to 'local')")
-			fs.String("address", "", "Address of the administration API listener (if --config is not used)")
-			fs.String("config", "", "Configuration file (if --address is not used)")
-			fs.String("adapter", "", "Name of config adapter to apply (if --config is used)")
-			return fs
-		}(),
+		CobraFunc: func(cmd *cobra.Command) {
+			cmd.Flags().StringP("cert", "p", "", "The path to the CA certificate to untrust")
+			cmd.Flags().StringP("ca", "", "", "The ID of the CA to untrust (defaults to 'local')")
+			cmd.Flags().StringP("address", "", "", "Address of the administration API listener (if --config is not used)")
+			cmd.Flags().StringP("config", "c", "", "Configuration file (if --address is not used)")
+			cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (if --config is used)")
+			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdUntrust)
+		},
 	})
 }
 
-- 
cgit v1.2.3