Skip to content

Commit

Permalink
Merge pull request #84 from hookdeck/listen-multiple
Browse files Browse the repository at this point in the history
v0.2.0
  • Loading branch information
leggetter authored Jul 29, 2024
2 parents 29564b2 + 6dd2063 commit c51251b
Show file tree
Hide file tree
Showing 12 changed files with 499 additions and 259 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ require (
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hookdeck/hookdeck-go-sdk v0.0.37 // indirect
github.com/hookdeck/hookdeck-go-sdk v0.4.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pty v1.1.8 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDG
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hookdeck/hookdeck-go-sdk v0.0.37 h1:Y+QnwsWuJ6KMkpY2qJZDeGzcKc4GkzBrRaEnIb8zimc=
github.com/hookdeck/hookdeck-go-sdk v0.0.37/go.mod h1:kfFn3/WEGcxuPkaaf8lAq9L+3nYg45GwGy4utH/Tnmg=
github.com/hookdeck/hookdeck-go-sdk v0.4.1 h1:r/rZJeBuDq31amTIB1LDHkA5lTAG2jAmZGqhgHRYKy8=
github.com/hookdeck/hookdeck-go-sdk v0.4.1/go.mod h1:kfFn3/WEGcxuPkaaf8lAq9L+3nYg45GwGy4utH/Tnmg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func selectShell(shell string) error {
}
return err
default:
return fmt.Errorf("Could not automatically detect your shell. Please run the command with the `--shell` flag for either bash or zsh")
return fmt.Errorf("could not automatically detect your shell. Please run the command with the `--shell` flag for either bash or zsh")
}
}

Expand Down
72 changes: 54 additions & 18 deletions pkg/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package cmd

import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
Expand All @@ -26,9 +27,9 @@ import (
)

type listenCmd struct {
cmd *cobra.Command
wsBaseURL string
noWSS bool
cmd *cobra.Command
noWSS bool
cliPath string
}

func newListenCmd() *listenCmd {
Expand All @@ -37,9 +38,15 @@ func newListenCmd() *listenCmd {
lc.cmd = &cobra.Command{
Use: "listen",
Short: "Forward events for a source to your local server",
Long: `Forward events for a source to your local server.
This command will create a new Hookdeck Source if it doesn't exist.
By default the Hookdeck Destination will be named "CLI", and the
Destination CLI path will be "/". To set the CLI path, use the "--cli-path" flag.`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("Requires a port or forwarding URL to forward the events to")
return errors.New("requires a port or forwarding URL to forward the events to")
}

_, err_port := strconv.ParseInt(args[0], 10, 64)
Expand All @@ -53,44 +60,72 @@ func newListenCmd() *listenCmd {
}

if err_port != nil && err_url != nil {
return errors.New("Argument is not a valid port or forwading URL")
return errors.New("argument is not a valid port or forwading URL")
}

if err_port != nil {
if parsed_url.Host == "" {
return errors.New("Forwarding URL must contain a host.")
return errors.New("forwarding URL must contain a host")
}

if parsed_url.RawQuery != "" {
return errors.New("Forwarding URL cannot contain query params.")
return errors.New("forwarding URL cannot contain query params")
}
}

if len(args) > 3 {
return errors.New("Invalid extra argument provided")
return errors.New("invalid extra argument provided")
}

return nil
},
RunE: lc.runListenCmd,
}
lc.cmd.Flags().BoolVar(&lc.noWSS, "no-wss", false, "Force unencrypted ws:// protocol instead of wss://")
lc.cmd.Flags().MarkHidden("no-wss")
lc.cmd.Flags().StringVar(&lc.cliPath, "cli-path", "", "Sets the server path of that locally running web server the events will be forwarded to")

usage := lc.cmd.UsageTemplate()

usage = strings.Replace(
usage,
"{{.UseLine}}",
`hookdeck listen [port or forwarding URL] [source] [connection] [flags]
Arguments:
- [port or forwarding URL]: Required. The port or forwarding URL to forward the events to e.g., "3000" or "http://localhost:3000"
- [source]: Required. The name of source to forward the events from e.g., "shopify", "stripe"
- [connection]: Optional. The name of the connection linking the Source and the Destination
`, 1)

usage += fmt.Sprintf(`
Examples:
Forward events from a Hookdeck Source named "shopify" to a local server running on port %[1]d:
hookdeck listen %[1]d shopify
Forward events to a local server running on "http://myapp.test":
hookdeck listen %[1]d http://myapp.test
Forward events to the path "/webhooks" on local server running on port %[1]d:
hookdeck listen %[1]d --cli-path /webhooks
`, 3000)

lc.cmd.SetUsageTemplate(
strings.Replace(
lc.cmd.UsageTemplate(),
"{{.UseLine}}",
"hookdeck listen [port or forwarding URL] [source] [connection] [flags]", 1),
)
lc.cmd.SetUsageTemplate(usage)

return lc
}

// listenCmd represents the listen command
func (lc *listenCmd) runListenCmd(cmd *cobra.Command, args []string) error {
var sourceAlias, connectionQuery string
var sourceQuery, connectionQuery string
if len(args) > 1 {
sourceAlias = args[1]
sourceQuery = args[1]
}
if len(args) > 2 {
connectionQuery = args[2]
Expand All @@ -112,7 +147,8 @@ func (lc *listenCmd) runListenCmd(cmd *cobra.Command, args []string) error {
url.Scheme = "http"
}

return listen.Listen(url, sourceAlias, connectionQuery, listen.Flags{
NoWSS: lc.noWSS,
return listen.Listen(url, sourceQuery, connectionQuery, listen.Flags{
NoWSS: lc.noWSS,
CliPath: lc.cliPath,
}, &Config)
}
2 changes: 0 additions & 2 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import (
"github.com/spf13/cobra"
)

var cfgFile string

var Config config.Config

var rootCmd = &cobra.Command{
Expand Down
3 changes: 1 addition & 2 deletions pkg/cmd/whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import (
)

type whoamiCmd struct {
cmd *cobra.Command
interactive bool
cmd *cobra.Command
}

func newWhoamiCmd() *whoamiCmd {
Expand Down
3 changes: 1 addition & 2 deletions pkg/hookdeck/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ type Session struct {
}

type CreateSessionInput struct {
SourceId string `json:"source_id"`
ConnectionIds []string `json:"webhook_ids"`
}

Expand All @@ -29,7 +28,7 @@ func (c *Client) CreateSession(input CreateSessionInput) (Session, error) {
if res.StatusCode != http.StatusOK {
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
return Session{}, fmt.Errorf("Unexpected http status code: %d %s", res.StatusCode, string(body))
return Session{}, fmt.Errorf("unexpected http status code: %d %s", res.StatusCode, string(body))
}
session := Session{}
postprocessJsonResponse(res, &session)
Expand Down
151 changes: 86 additions & 65 deletions pkg/listen/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,93 +2,114 @@ package listen

import (
"context"
"errors"
"fmt"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/gosimple/slug"
hookdecksdk "github.com/hookdeck/hookdeck-go-sdk"
hookdeckclient "github.com/hookdeck/hookdeck-go-sdk/client"
log "github.com/sirupsen/logrus"
)

func getConnections(client *hookdeckclient.Client, source *hookdecksdk.Source, connectionQuery string) ([]*hookdecksdk.Connection, error) {
// TODO: Filter connections using connectionQuery
var connections []*hookdecksdk.Connection
connectionList, err := client.Connection.List(context.Background(), &hookdecksdk.ConnectionListRequest{
SourceId: &source.Id,
func getConnections(client *hookdeckclient.Client, sources []*hookdecksdk.Source, connectionFilterString string, isMultiSource bool, cliPath string) ([]*hookdecksdk.Connection, error) {
sourceIDs := []*string{}

for _, source := range sources {
sourceIDs = append(sourceIDs, &source.Id)
}

connectionQuery, err := client.Connection.List(context.Background(), &hookdecksdk.ConnectionListRequest{
SourceId: sourceIDs,
})
if err != nil {
return nil, err
return []*hookdecksdk.Connection{}, err
}
connections = connectionList.Models

var filteredConnections []*hookdecksdk.Connection
connections, err := filterConnections(connectionQuery.Models, connectionFilterString)
if err != nil {
return []*hookdecksdk.Connection{}, err
}

connections, err = ensureConnections(client, connections, sources, isMultiSource, connectionFilterString, cliPath)
if err != nil {
return []*hookdecksdk.Connection{}, err
}

return connections, nil
}

// 1. Filter to only include CLI destination
// 2. Apply connectionFilterString
func filterConnections(connections []*hookdecksdk.Connection, connectionFilterString string) ([]*hookdecksdk.Connection, error) {
// 1. Filter to only include CLI destination
var cliDestinationConnections []*hookdecksdk.Connection
for _, connection := range connections {
if connection.Destination.CliPath != nil && *connection.Destination.CliPath != "" {
filteredConnections = append(filteredConnections, connection)
cliDestinationConnections = append(cliDestinationConnections, connection)
}
}
connections = filteredConnections

if connectionQuery != "" {
is_path, err := isPath(connectionQuery)
if err != nil {
return connections, err
}
var filteredConnections []*hookdecksdk.Connection
for _, connection := range connections {
if (is_path && connection.Destination.CliPath != nil && strings.Contains(*connection.Destination.CliPath, connectionQuery)) || (connection.Name != nil && *connection.Name == connectionQuery) {
filteredConnections = append(filteredConnections, connection)
}
}
connections = filteredConnections
if connectionFilterString == "" {
return cliDestinationConnections, nil
}

if len(connections) == 0 {
answers := struct {
Label string `survey:"label"`
Path string `survey:"path"`
}{}
var qs = []*survey.Question{
{
Name: "path",
Prompt: &survey.Input{Message: "What path should the events be forwarded to (ie: /webhooks)?"},
Validate: func(val interface{}) error {
str, ok := val.(string)
is_path, err := isPath(str)
if !ok || !is_path || err != nil {
return errors.New("invalid path")
}
return nil
},
},
{
Name: "label",
Prompt: &survey.Input{Message: "What's your connection label (ie: My API)?"},
Validate: survey.Required,
},
// 2. Apply connectionFilterString
isPath, err := isPath(connectionFilterString)
if err != nil {
return connections, err
}
var filteredConnections []*hookdecksdk.Connection
for _, connection := range cliDestinationConnections {
if (isPath && connection.Destination.CliPath != nil && strings.Contains(*connection.Destination.CliPath, connectionFilterString)) || (connection.Name != nil && *connection.Name == connectionFilterString) {
filteredConnections = append(filteredConnections, connection)
}
}

err := survey.Ask(qs, &answers)
if err != nil {
fmt.Println(err.Error())
return connections, err
}
alias := slug.Make(answers.Label)
connection, err := client.Connection.Create(context.Background(), &hookdecksdk.ConnectionCreateRequest{
Name: hookdecksdk.OptionalOrNull(&alias),
SourceId: hookdecksdk.OptionalOrNull(&source.Id),
Destination: hookdecksdk.OptionalOrNull(&hookdecksdk.ConnectionCreateRequestDestination{
Name: alias,
CliPath: &answers.Path,
}),
})
if err != nil {
return connections, err
}
connections = append(connections, connection)
return filteredConnections, nil
}

// When users want to listen to a single source but there is no connection for that source,
// we can help user set up a new connection for it.
func ensureConnections(client *hookdeckclient.Client, connections []*hookdecksdk.Connection, sources []*hookdecksdk.Source, isMultiSource bool, connectionFilterString string, cliPath string) ([]*hookdecksdk.Connection, error) {
if len(connections) > 0 || isMultiSource {
log.Debug(fmt.Sprintf("Connection exists for Source \"%s\", Connection \"%s\", and CLI path \"%s\"", sources[0].Name, connectionFilterString, cliPath))

return connections, nil
}

log.Debug(fmt.Sprintf("No connection found. Creating a connection for Source \"%s\", Connection \"%s\", and CLI path \"%s\"", sources[0].Name, connectionFilterString, cliPath))

connectionDetails := struct {
Label string `survey:"label"`
Path string `survey:"path"`
}{}

if len(connectionFilterString) == 0 {
connectionDetails.Label = "cli"
} else {
connectionDetails.Label = connectionFilterString
}

if len(cliPath) == 0 {
connectionDetails.Path = "/"
} else {
connectionDetails.Path = cliPath
}

alias := slug.Make(connectionDetails.Label)

connection, err := client.Connection.Create(context.Background(), &hookdecksdk.ConnectionCreateRequest{
Name: hookdecksdk.OptionalOrNull(&alias),
SourceId: hookdecksdk.OptionalOrNull(&sources[0].Id),
Destination: hookdecksdk.OptionalOrNull(&hookdecksdk.ConnectionCreateRequestDestination{
Name: alias,
CliPath: &connectionDetails.Path,
}),
})
if err != nil {
return connections, err
}
connections = append(connections, connection)

return connections, nil
}
Loading

0 comments on commit c51251b

Please sign in to comment.