Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1480,7 +1480,8 @@ There are also hidden flags for development and debugging (not listed in user-fa

* `--api-base`: Sets the API base URL.
* `--dashboard-base`: Sets the web dashboard base URL.
* `--console-base`: Sets the web console base URL.
* `--console-base`: Deprecated. Guest links now open Hookdeck Inspect; use `--inspect-base`.
* `--inspect-base`: Sets the Hookdeck Inspect base URL that `listen` links guest accounts to.
* `--ws-base`: Sets the Websocket base URL.

## Troubleshooting
Expand Down
8 changes: 7 additions & 1 deletion pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ var flagNeedsNextArg = map[string]bool{
"outpost-api-base": true,
"dashboard-base": true,
"console-base": true,
"inspect-base": true,
"ws-base": true,
}

Expand Down Expand Up @@ -401,7 +402,12 @@ func init() {
rootCmd.PersistentFlags().MarkHidden("dashboard-base")

rootCmd.PersistentFlags().StringVar(&Config.ConsoleBaseURL, "console-base", "", fmt.Sprintf("Sets the web console base URL (default \"%s\")", hookdeck.DefaultConsoleBaseURL))
rootCmd.PersistentFlags().MarkHidden("console-base")
// Nothing reads it since guest links moved to Hookdeck Inspect. Kept so existing
// scripts don't fail on an unknown flag; cobra hides it and prints a warning.
rootCmd.PersistentFlags().MarkDeprecated("console-base", "guest links now open Hookdeck Inspect; use --inspect-base instead")

rootCmd.PersistentFlags().StringVar(&Config.InspectBaseURL, "inspect-base", "", fmt.Sprintf("Sets the Hookdeck Inspect base URL, used for guest links (default \"%s\")", hookdeck.DefaultInspectBaseURL))
rootCmd.PersistentFlags().MarkHidden("inspect-base")

rootCmd.PersistentFlags().StringVar(&Config.WSBaseURL, "ws-base", "", fmt.Sprintf("Sets the Websocket base URL (default \"%s\")", hookdeck.DefaultWebsocektURL))
rootCmd.PersistentFlags().MarkHidden("ws-base")
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Config struct {
OutpostAPIBaseURL string
DashboardBaseURL string
ConsoleBaseURL string
InspectBaseURL string
WSBaseURL string
Insecure bool

Expand Down Expand Up @@ -404,6 +405,7 @@ func (c *Config) constructConfig() {
c.OutpostAPIBaseURL = stringCoalesce(c.OutpostAPIBaseURL, c.viper.GetString(("outpost_api_base")), hookdeck.DefaultOutpostAPIBaseURL)
c.DashboardBaseURL = stringCoalesce(c.DashboardBaseURL, c.viper.GetString(("dashboard_base")), hookdeck.DefaultDashboardBaseURL)
c.ConsoleBaseURL = stringCoalesce(c.ConsoleBaseURL, c.viper.GetString(("console_base")), hookdeck.DefaultConsoleBaseURL)
c.InspectBaseURL = stringCoalesce(c.InspectBaseURL, c.viper.GetString(("inspect_base")), hookdeck.DefaultInspectBaseURL)
c.WSBaseURL = stringCoalesce(c.WSBaseURL, c.viper.GetString(("ws_base")), hookdeck.DefaultWebsocektURL)
c.Profile.Name = stringCoalesce(c.Profile.Name, c.viper.GetString(("profile")), hookdeck.DefaultProfileName)
// Needs to support both profile-based config
Expand Down
5 changes: 5 additions & 0 deletions pkg/hookdeck/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ const DefaultDashboardURL = "https://dashboard.hookdeck.com"
// DefaultDashboardBaseURL is the default base URL for dashboard requests
const DefaultDashboardBaseURL = "https://dashboard.hookdeck.com"

// DefaultConsoleBaseURL is the default base URL for Hookdeck Console.
const DefaultConsoleBaseURL = "https://console.hookdeck.com"

// DefaultInspectBaseURL is the default base URL for Hookdeck Inspect, which
// listen links to for guest accounts (project type "console").
const DefaultInspectBaseURL = "https://inspect.hookdeck.com"

const DefaultWebsocektURL = "wss://ws.hookdeck.com"

const DefaultProfileName = "default"
Expand Down
153 changes: 119 additions & 34 deletions pkg/listen/links/links.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,131 @@
// Package links builds dashboard/console deep-links for listen output. It is
// shared by the interactive TUI and the compact/quiet printer so the two
// output modes produce identical, correctly scoped URLs.
// Package links builds the web links listen prints: Event Gateway dashboard
// links for regular projects, and Hookdeck Inspect links for guest accounts
// (project type "console"). It is shared by the interactive TUI and the
// compact/quiet output so every output mode produces identical, correctly
// scoped URLs.
package links

// DashboardHome returns the dashboard (or console) events link for the
// session, omitting the team_id parameter when the project id is unknown so
// links never render with an empty value.
func DashboardHome(dashboardBaseURL, consoleBaseURL, projectType, projectID string) string {
if projectType == "console" {
if projectID == "" {
return consoleBaseURL
}
return consoleBaseURL + "?team_id=" + projectID
import (
"net/url"
"strings"

"github.com/hookdeck/hookdeck-cli/pkg/config"
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
)

// Target describes where a listen session's links point.
type Target struct {
// DashboardBaseURL is the Event Gateway dashboard, used for every project
// type except console.
DashboardBaseURL string
// InspectBaseURL is Hookdeck Inspect, used for guest (console) projects.
InspectBaseURL string
// ProjectType is the profile's project type (config.ProjectType*).
ProjectType string
// ProjectID is the project's team id. Dashboard links are scoped by it,
// and Inspect event links carry it so Inspect can check the visitor is in
// that project.
ProjectID string
}

func (t Target) inspect() bool {
return t.ProjectType == config.ProjectTypeConsole
}

// Name returns what the session's links open, for the labels shown next to
// them: "Hookdeck Inspect" for guest accounts, "dashboard" otherwise.
func (t Target) Name() string {
if t.inspect() {
return "Hookdeck Inspect"
}
return "dashboard"
}

// HomeHint returns the sentence printed before the Home link. The dashboard
// wording is unchanged; Inspect shows the requests a Source received.
func (t Target) HomeHint(dashboardHint string) string {
if t.inspect() {
return "View your requests in Hookdeck Inspect"
}
return dashboardHint
}

// Home returns the link to the session's events. For the dashboard that is
// the CLI events view, scoped by team_id when the project id is known. For
// Inspect it is the page of sourceID, or the Inspect home page when sourceID
// is empty (a session listening to several Sources has no single page).
func (t Target) Home(sourceID string) string {
if t.inspect() {
return inspectSource(t.InspectBaseURL, sourceID)
}
if projectID == "" {
return dashboardBaseURL + "/events/cli"
if t.ProjectID == "" {
return t.DashboardBaseURL + "/events/cli"
}
return dashboardBaseURL + "/events/cli?team_id=" + projectID
return t.DashboardBaseURL + "/events/cli?team_id=" + t.ProjectID
}

// DashboardHomeDisplay returns the display text for the DashboardHome link:
// the same destination without the team_id query parameter.
func DashboardHomeDisplay(dashboardBaseURL, consoleBaseURL, projectType string) string {
if projectType == "console" {
return consoleBaseURL
// HomeDisplay returns the display text for the Home link: the same
// destination without the team_id query parameter.
func (t Target) HomeDisplay(sourceID string) string {
if t.inspect() {
return inspectSource(t.InspectBaseURL, sourceID)
}
return dashboardBaseURL + "/events/cli"
return t.DashboardBaseURL + "/events/cli"
}

// Event returns the dashboard (or console) deep-link for a single event,
// omitting the team_id parameter when the project id is unknown.
func Event(dashboardBaseURL, consoleBaseURL, projectType, projectID, eventID string) string {
if projectType == "console" {
url := consoleBaseURL + "/?event_id=" + eventID
if projectID != "" {
url += "&team_id=" + projectID
}
return url
// EventURL returns the deep-link for a single event. For the dashboard that is
// the event page, scoped by team_id when the project id is known. For Inspect
// it is the home page with ?event_id= and ?team_id=: Inspect looks the event
// up and opens the request that created it on its Source's page.
func (t Target) EventURL(eventID string) string {
if t.inspect() {
return inspectEvent(t.InspectBaseURL, eventID, t.ProjectID)
}
url := dashboardBaseURL + "/events/" + eventID
if projectID != "" {
url += "?team_id=" + projectID
link := t.DashboardBaseURL + "/events/" + eventID
if t.ProjectID != "" {
link += "?team_id=" + t.ProjectID
}
return link
}

// EventDisplay returns the display text for the EventURL link, as printed at
// the end of each event log line. The dashboard shows the event page without
// team_id. Inspect shows the full link, team_id included, because Inspect
// needs both ids to open the event.
func (t Target) EventDisplay(eventID string) string {
if t.inspect() {
return t.EventURL(eventID)
}
return t.DashboardBaseURL + "/events/" + eventID
}

// inspectSource builds an Inspect Source page link. Inspect addresses a Source
// by its id without the "src_" prefix. Without a Source it falls back to the
// Inspect home page.
func inspectSource(inspectBaseURL, sourceID string) string {
bare := strings.TrimPrefix(sourceID, "src_")
if bare == "" {
return inspectBaseURL
}
return strings.TrimSuffix(inspectBaseURL, "/") + "/" + url.PathEscape(bare)
}

// inspectEvent builds an Inspect event link: the home page with event_id and,
// when known, team_id. These are the parameters Console's event links used.
func inspectEvent(inspectBaseURL, eventID, teamID string) string {
query := url.Values{}
query.Set("event_id", eventID)
if teamID != "" {
query.Set("team_id", teamID)
}
return strings.TrimSuffix(inspectBaseURL, "/") + "/?" + query.Encode()
}

// SessionSourceID returns the id of the one Source a session listens to, or
// "" when it listens to none or to several.
func SessionSourceID(sources []*hookdeck.Source) string {
if len(sources) != 1 || sources[0] == nil {
return ""
}
return url
return sources[0].ID
}
95 changes: 75 additions & 20 deletions pkg/listen/links/links_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,110 @@ import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
)

const (
dashboardBase = "https://dashboard.hookdeck.com"
consoleBase = "https://console.hookdeck.com"
inspectBase = "https://inspect.hookdeck.com"
)

func TestDashboardHome(t *testing.T) {
func dashboardTarget(projectID string) Target {
return Target{DashboardBaseURL: dashboardBase, InspectBaseURL: inspectBase, ProjectType: "gateway", ProjectID: projectID}
}

func inspectTarget(projectID string) Target {
return Target{DashboardBaseURL: dashboardBase, InspectBaseURL: inspectBase, ProjectType: "console", ProjectID: projectID}
}

func TestHome(t *testing.T) {
t.Run("dashboard with project id", func(t *testing.T) {
assert.Equal(t, "https://dashboard.hookdeck.com/events/cli?team_id=tm_123", DashboardHome(dashboardBase, consoleBase, "inbound", "tm_123"))
assert.Equal(t, "https://dashboard.hookdeck.com/events/cli?team_id=tm_123", dashboardTarget("tm_123").Home("src_abc123def456"))
})

t.Run("dashboard without project id omits team_id", func(t *testing.T) {
assert.Equal(t, "https://dashboard.hookdeck.com/events/cli", DashboardHome(dashboardBase, consoleBase, "inbound", ""))
assert.Equal(t, "https://dashboard.hookdeck.com/events/cli", dashboardTarget("").Home(""))
})

t.Run("inspect with one source links to the source page without src_ or team_id", func(t *testing.T) {
assert.Equal(t, "https://inspect.hookdeck.com/abc123def456", inspectTarget("tm_123").Home("src_abc123def456"))
})

t.Run("console with project id", func(t *testing.T) {
assert.Equal(t, "https://console.hookdeck.com?team_id=tm_123", DashboardHome(dashboardBase, consoleBase, "console", "tm_123"))
t.Run("inspect without a single source links to inspect home", func(t *testing.T) {
assert.Equal(t, "https://inspect.hookdeck.com", inspectTarget("tm_123").Home(""))
})

t.Run("console without project id omits team_id", func(t *testing.T) {
assert.Equal(t, "https://console.hookdeck.com", DashboardHome(dashboardBase, consoleBase, "console", ""))
t.Run("inspect base with trailing slash", func(t *testing.T) {
target := inspectTarget("")
target.InspectBaseURL = "https://inspect-staging.hookdeck.com/"
assert.Equal(t, "https://inspect-staging.hookdeck.com/abc123def456", target.Home("src_abc123def456"))
})
}

func TestDashboardHomeDisplay(t *testing.T) {
t.Run("dashboard shows events path", func(t *testing.T) {
assert.Equal(t, "https://dashboard.hookdeck.com/events/cli", DashboardHomeDisplay(dashboardBase, consoleBase, "inbound"))
func TestName(t *testing.T) {
assert.Equal(t, "Hookdeck Inspect", inspectTarget("tm_123").Name())
assert.Equal(t, "dashboard", dashboardTarget("tm_123").Name())
}

func TestHomeHint(t *testing.T) {
assert.Equal(t, "View your requests in Hookdeck Inspect", inspectTarget("").HomeHint("Open dashboard"))
assert.Equal(t, "Open dashboard", dashboardTarget("").HomeHint("Open dashboard"))
}

func TestHomeDisplay(t *testing.T) {
t.Run("dashboard shows events path without team_id", func(t *testing.T) {
assert.Equal(t, "https://dashboard.hookdeck.com/events/cli", dashboardTarget("tm_123").HomeDisplay("src_abc123def456"))
})

t.Run("console shows console base", func(t *testing.T) {
assert.Equal(t, "https://console.hookdeck.com", DashboardHomeDisplay(dashboardBase, consoleBase, "console"))
t.Run("inspect shows the source page", func(t *testing.T) {
assert.Equal(t, "https://inspect.hookdeck.com/abc123def456", inspectTarget("tm_123").HomeDisplay("src_abc123def456"))
})

t.Run("inspect without a single source shows inspect home", func(t *testing.T) {
assert.Equal(t, "https://inspect.hookdeck.com", inspectTarget("tm_123").HomeDisplay(""))
})
}

func TestEvent(t *testing.T) {
func TestEventURL(t *testing.T) {
t.Run("dashboard with project id", func(t *testing.T) {
assert.Equal(t, "https://dashboard.hookdeck.com/events/evt_1?team_id=tm_123", Event(dashboardBase, consoleBase, "inbound", "tm_123", "evt_1"))
assert.Equal(t, "https://dashboard.hookdeck.com/events/evt_1?team_id=tm_123", dashboardTarget("tm_123").EventURL("evt_1"))
})

t.Run("dashboard without project id omits team_id", func(t *testing.T) {
assert.Equal(t, "https://dashboard.hookdeck.com/events/evt_1", Event(dashboardBase, consoleBase, "inbound", "", "evt_1"))
assert.Equal(t, "https://dashboard.hookdeck.com/events/evt_1", dashboardTarget("").EventURL("evt_1"))
})

t.Run("inspect links to the event by event_id and team_id", func(t *testing.T) {
assert.Equal(t, "https://inspect.hookdeck.com/?event_id=evt_1&team_id=tm_123", inspectTarget("tm_123").EventURL("evt_1"))
})

t.Run("console with project id", func(t *testing.T) {
assert.Equal(t, "https://console.hookdeck.com/?event_id=evt_1&team_id=tm_123", Event(dashboardBase, consoleBase, "console", "tm_123", "evt_1"))
t.Run("inspect without project id omits team_id", func(t *testing.T) {
assert.Equal(t, "https://inspect.hookdeck.com/?event_id=evt_1", inspectTarget("").EventURL("evt_1"))
})

t.Run("console without project id omits team_id", func(t *testing.T) {
assert.Equal(t, "https://console.hookdeck.com/?event_id=evt_1", Event(dashboardBase, consoleBase, "console", "", "evt_1"))
t.Run("inspect base with trailing slash", func(t *testing.T) {
target := inspectTarget("tm_123")
target.InspectBaseURL = "https://inspect-staging.hookdeck.com/"
assert.Equal(t, "https://inspect-staging.hookdeck.com/?event_id=evt_1&team_id=tm_123", target.EventURL("evt_1"))
})
}

func TestEventDisplay(t *testing.T) {
t.Run("dashboard shows the event page without team_id", func(t *testing.T) {
assert.Equal(t, "https://dashboard.hookdeck.com/events/evt_1", dashboardTarget("tm_123").EventDisplay("evt_1"))
})

t.Run("inspect shows the same link it opens, team_id included", func(t *testing.T) {
assert.Equal(t, "https://inspect.hookdeck.com/?event_id=evt_1&team_id=tm_123", inspectTarget("tm_123").EventDisplay("evt_1"))
})
}

func TestSessionSourceID(t *testing.T) {
one := &hookdeck.Source{ID: "src_1"}
two := &hookdeck.Source{ID: "src_2"}

assert.Equal(t, "src_1", SessionSourceID([]*hookdeck.Source{one}))
assert.Equal(t, "", SessionSourceID([]*hookdeck.Source{one, two}), "several sources have no single page")
assert.Equal(t, "", SessionSourceID(nil))
}
4 changes: 2 additions & 2 deletions pkg/listen/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ Specify a single destination to update the path. For example, pass a connection
ProjectType: config.Profile.ProjectType,
APIBaseURL: config.APIBaseURL,
DashboardBaseURL: config.DashboardBaseURL,
ConsoleBaseURL: config.ConsoleBaseURL,
InspectBaseURL: config.InspectBaseURL,
WSBaseURL: config.WSBaseURL,
NoWSS: flags.NoWSS,
NoHealthcheck: flags.NoHealthcheck,
Expand All @@ -194,7 +194,7 @@ Specify a single destination to update the path. For example, pass a connection
APIKey: config.Profile.APIKey,
APIBaseURL: config.APIBaseURL,
DashboardBaseURL: config.DashboardBaseURL,
ConsoleBaseURL: config.ConsoleBaseURL,
InspectBaseURL: config.InspectBaseURL,
ProjectType: config.Profile.ProjectType,
ProjectID: projectID,
GuestURL: guestURL,
Expand Down
Loading
Loading