Skip to content

Commit

Permalink
CLI for interacting with the errands service (#8)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Dursun <[email protected]>
  • Loading branch information
joedursun and joedursun authored Jun 30, 2022
1 parent fd8e91c commit 504b896
Show file tree
Hide file tree
Showing 8 changed files with 807 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

vendor/
vendor/
45 changes: 45 additions & 0 deletions cmd/errands/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# CLI Overview

The `errands` CLI facilitates working with the errands API. It provides commands
such as `list` and `delete` and can even port-forward the errands service in our
k8s cluster so you don't have to!

# Installation

If you just want to use the tool as it is, you can install it by running
```bash
go install github.com/polygon-io/errands-go/cmd/errands@latest
errands help
```

If you've got the repo cloned and are dev-ing on the tool, you can build and run it locally:
```bash
go run ./cmd/errands/. help # assuming you're in this directory
```

or install your local version:
```bash
go install ./cmd/errands/. # assuming you're in this directory
errands help
```

# Usage

By default it will `kubectl port-forward` the errands service on `localhost:5555` but you can change the
port via `--port=XXX` or disable the port-forwarding entirely via `--bootstrap=false`.

If you disable bootstrapping then you'll need to provide the endpoint via `--endpoint=http://my-running-errands-endpoint.com`.

```bash
# to list all the failed or inactive sort-pparc errands
errands list --type=sort-pparc --status=failed,inactive

# if you're already port-forwarding the errands service on port 6000
errands list --type=sort-pparc --status=failed,inactive --bootstrap=false --port=6000

# to perform a dry-run delete of all the failed sort-pparc jobs
errands delete --type=sort-pparc --status=failed --dry-run=true

# to delete an errand by its ID
errands delete --id=abc-xyz-123
```
82 changes: 82 additions & 0 deletions cmd/errands/cmd/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package cmd

import (
"context"
"fmt"
"os"

errandz "github.com/polygon-io/errands-go"
"github.com/spf13/cobra"
)

func (ec *errandsCmd) newDeleteCommand() (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "delete",
Short: "deletes errands by ID, type, or status",
RunE: ec.delete,
PreRunE: ec.bindViperFlagsPreRun,
}

cmd.Flags().String("type", "", "Filter by errand type")
cmd.Flags().String("status", "failed", "Filter by status; comma delimited")
cmd.Flags().String("id", "", "ID of the errand to delete")
cmd.Flags().Bool("dry-run", false, "Don't actually delete anything. Only used for bulk deletion")

return cmd, nil
}

func (ec *errandsCmd) delete(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

if ec.viper.GetBool("bootstrap") {
portCmd, err := ec.portForwardErrandsServer(ctx)
if err != nil {
return fmt.Errorf("port-forward: %w", err)
}

defer func() {
if err := portCmd.Process.Signal(os.Interrupt); err != nil {
fmt.Printf("error killing port-forward: %s\n", err)
}
}()
}

if id := ec.viper.GetString("id"); id != "" {
if err := deleteErrand(ec.api, id); err != nil {
return fmt.Errorf("failed to delete errand %s: %w", id, err)
}

return nil
}

jobs, err := listErrandsForTopic(ec.api, ec.viper.GetString("type"), ec.viper.GetString("status"))
if err != nil {
return fmt.Errorf("failed to get errands: %w", err)
}

for _, job := range jobs {
name := job.Name
if len(name) > 100 {
name = name[:100]
}

if ec.viper.GetBool("dry-run") {
fmt.Printf("(dry-run) delete %s: (%s) %s\n", job.ID, job.Status, name)
continue
}

fmt.Printf("deleting %s\n", job.ID)
if err := deleteErrand(ec.api, job.ID); err != nil {
fmt.Printf("failed to delete errand %s: %e", job.ID, err)
}
}

return nil
}

func deleteErrand(api *errandz.ErrandsAPI, id string) error {
_, err := api.DeleteErrand(id)

return err
}
102 changes: 102 additions & 0 deletions cmd/errands/cmd/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cmd

import (
"context"
"errors"
"fmt"
"os"
"sort"
"strings"
"time"

errandz "github.com/polygon-io/errands-go"
"github.com/polygon-io/errands-server/schemas"
"github.com/spf13/cobra"
)

func (ec *errandsCmd) newListCommand() (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "list",
Short: "returns a list of errands from our errands API",
PreRunE: ec.bindViperFlagsPreRun,
RunE: ec.run,
}

cmd.Flags().String("type", "", "filter by errand type")
cmd.Flags().String("status", "", "filter by status; comma delimited")
cmd.Flags().Int("port", 5555, "localhost port for the errands server")

return cmd, nil
}

func (ec *errandsCmd) run(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

if ec.viper.GetBool("bootstrap") {
portCmd, err := ec.portForwardErrandsServer(ctx)
if err != nil {
return fmt.Errorf("port-forward: %w", err)
}

defer func() {
if err := portCmd.Process.Signal(os.Interrupt); err != nil {
fmt.Printf("error killing port-forward: %s\n", err)
}
}()
}

jobs, err := listErrandsForTopic(ec.api, ec.viper.GetString("type"), ec.viper.GetString("status"))
if err != nil {
return fmt.Errorf("get errands: %w", err)
}

for _, job := range jobs {
name := job.Name
if len(name) > 100 {
name = name[:100]
}
fmt.Printf("%100s: %10s | %s | %s\n", name, job.Status, job.ID, time.UnixMilli(job.Created))
}

return nil
}

func filterByStatus(jobs []schemas.Errand, status string) []schemas.Errand {
if status == "" {
return jobs
}

statusFilter := make(map[schemas.Status]bool)
for _, s := range strings.Split(status, ",") {
statusFilter[schemas.Status(s)] = true
}

var filtered []schemas.Errand
for _, job := range jobs {
if statusFilter[job.Status] {
filtered = append(filtered, job)
}
}

return filtered
}

func listErrandsForTopic(api *errandz.ErrandsAPI, errandType string, status string) ([]schemas.Errand, error) {
if errandType == "" {
return nil, errors.New("errand type is required")
}

jobs, err := api.ListErrands("type", errandType)
if err != nil {
return nil, err
}

results := filterByStatus(jobs.Results, status)

sort.Slice(results, func(i, j int) bool {
return results[i].Created > results[j].Created
})

return results, nil
}
Loading

0 comments on commit 504b896

Please sign in to comment.