Skip to content

Instantly share code, notes, and snippets.

@smoya
Last active May 19, 2020 18:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save smoya/35776ce152301b938a43efc637fb4bd8 to your computer and use it in GitHub Desktop.
Save smoya/35776ce152301b938a43efc637fb4bd8 to your computer and use it in GitHub Desktop.
[Go] POC - Expose spf13/cobra commands via HTTP

[Go] Expose spf13/cobra commands via HTTP poc (do not do this at home)

This poc tries to demonstrate how "easy" can be exposing CLI tools via HTTP API. The example isn't perfect. It doesn't provide a REST API but it just allow to quickly prototype tooling that can be accessible both via CLI or HTTP.

This is a do not do this at home, however it can be used as starting point for interesting approaches.

A bit of context

I decided to do this insane proof of concept in order to (in)validate the idea of exposing cobra commands via HTTP as a quick prototyping framework. Once built, you just need to create commands and the api will expose them. Even though it could be fast for prototyping, this is not the ideal solution as it's super tricky.

My recommendation would be letting the business logic be out of the commands first, then both Cobra commands and API handlers using that logic instead. Another good approach in case the code grows, would be to take a piece of CQRS, declare use cases (Commands and Queries) as structs, and create adapters for the CLI and the HTTP requests, then execute the right Command or Query business logic.

Demo

demo

Video version

package main
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
"github.com/spf13/cobra"
)
func ServeCommand() *cobra.Command {
cmd := cobra.Command{
Use: "serve",
RunE: func(_ *cobra.Command, args []string) error {
log.Println("listening on port 8080...")
return http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Positional arguments are implicit in the path.
// Flags (both single our double dashed) are sent via query params. Dash(es) needed.
// Example: myservice:8080/ban/30.40.50.120?-v=true&--dry-run=true
args := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
// append query params as simulating flags
for k, vs := range r.URL.Query() {
for _, v := range vs {
if !strings.HasPrefix(k, "-") {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Please add -- or - to the flags"))
return
}
args = append(args, fmt.Sprintf("%s=%s", k, v))
}
}
buf := new(bytes.Buffer)
rootCmd := RootCmd()
rootCmd.DisableSuggestions = true
rootCmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error {
w.WriteHeader(http.StatusBadRequest)
log.Printf("malformed query params. Err: %s", err)
w.Write([]byte(err.Error()))
return err
})
// TODO There are more improvements we can do around returning 400.
rootCmd.SetOut(buf) // capture the output in order to properly set the headers before writing the response. Can be improved i guess.
rootCmd.SetArgs(args)
c, err := rootCmd.ExecuteC()
if err != nil {
if c == nil || c.Name() == rootCmd.Name() { // command not found
w.WriteHeader(http.StatusBadRequest)
log.Printf("command not found. Err: %s", err)
} else {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("error executing command. Command: %s, Err: %s", c.UseLine(), err)
}
w.Write([]byte(err.Error()))
return
}
log.Println("Executed command:", c.UseLine())
io.Copy(w, buf)
}))
},
}
return &cmd
}
func BanIPCommand() *cobra.Command {
var verbose, dryRun bool
cmd := cobra.Command{
Use: "ban",
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
if args[0] != "34.55.123.16" { // Just to force an err the example
return errors.New("this is just a demo. Only 34.55.123.16 ip is allowed")
}
if verbose {
c.Printf("IP %s got banned\n", args[0])
}
// obviously this is just a simulation...
if dryRun {
c.Println("No operation performed as dry-run")
}
return nil
},
}
cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
cmd.PersistentFlags().BoolVarP(&dryRun, "dry-run", "d", false, "dry-run meaning no operation will be performed")
return &cmd
}
func UnbanIPCommand() *cobra.Command {
var verbose, dryRun bool
cmd := cobra.Command{
Use: "unban",
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
if args[0] != "34.55.123.16" { // Just to force an err the example
return errors.New("this is just a demo. Only 34.55.123.16 ip is allowed")
}
if verbose {
c.Printf("IP %s got unbanned\n", args[0])
}
// obviously this is just a simulation...
if dryRun {
c.Println("No operation performed as dry-run")
}
return nil
},
}
cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
cmd.PersistentFlags().BoolVarP(&dryRun, "dry-run", "d", false, "dry-run meaning no operation will be performed")
return &cmd
}
func RootCmd() *cobra.Command {
rootCmd := cobra.Command{
Use: "dd8",
}
rootCmd.AddCommand(BanIPCommand(), UnbanIPCommand(), ServeCommand())
return &rootCmd
}
func main() {
rootCmd := RootCmd()
if err := rootCmd.Execute(); err != nil {
log.Fatalf("Failed to start: %v", err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment