From fc43235022f95d6bad05878219b6fe1840c48efa Mon Sep 17 00:00:00 2001 From: krakenhavoc Date: Wed, 25 Mar 2026 13:22:58 +0000 Subject: [PATCH 1/4] feat: add support for probes and endpoints --- cmd/krakenkey/main.go | 153 +++++++++++++++++++++++++++++++++ internal/api/client.go | 58 +++++++++++++ internal/api/types.go | 22 +++++ internal/endpoint/endpoint.go | 156 ++++++++++++++++++++++++++++++++++ 4 files changed, 389 insertions(+) create mode 100644 internal/endpoint/endpoint.go diff --git a/cmd/krakenkey/main.go b/cmd/krakenkey/main.go index 04c06a4..06c42ec 100644 --- a/cmd/krakenkey/main.go +++ b/cmd/krakenkey/main.go @@ -18,6 +18,7 @@ import ( "github.com/krakenkey/cli/internal/cert" "github.com/krakenkey/cli/internal/config" "github.com/krakenkey/cli/internal/domain" + "github.com/krakenkey/cli/internal/endpoint" "github.com/krakenkey/cli/internal/output" ) @@ -113,6 +114,8 @@ func run() int { cmdErr = runAccount(ctx, client, printer, subArgs) case "cert": cmdErr = runCert(ctx, client, printer, cfg, subArgs) + case "endpoint": + cmdErr = runEndpoint(ctx, client, printer, subArgs) default: fmt.Fprintf(os.Stderr, "error: unknown command %q — run 'krakenkey help'\n", cmd) return 1 @@ -636,6 +639,133 @@ func runCert(ctx context.Context, client *api.Client, printer *output.Printer, c } } +// ── endpoint ────────────────────────────────────────────────────────────────── + +func runEndpoint(ctx context.Context, client *api.Client, printer *output.Printer, args []string) error { + if len(args) == 0 || args[0] == "--help" || args[0] == "-h" { + fmt.Print(endpointUsage) + return nil + } + + sub := args[0] + subArgs := args[1:] + + switch sub { + case "add": + fs := flag.NewFlagSet("endpoint add", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + var ( + port int + sniFlag string + labelFlag string + ) + fs.IntVar(&port, "port", 443, "Port to monitor (default: 443)") + fs.StringVar(&sniFlag, "sni", "", "SNI override (optional)") + fs.StringVar(&labelFlag, "label", "", "Label (optional)") + fs.Usage = func() { + fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint add [--port 443] [--sni host] [--label name]\n") + } + if err := fs.Parse(subArgs); err != nil { + return err + } + if fs.NArg() == 0 { + return &api.ErrConfig{Message: "hostname is required"} + } + var sni, label *string + if sniFlag != "" { + sni = &sniFlag + } + if labelFlag != "" { + label = &labelFlag + } + return endpoint.RunAdd(ctx, client, printer, fs.Arg(0), port, sni, label) + + case "list": + return endpoint.RunList(ctx, client, printer) + + case "show": + fs := flag.NewFlagSet("endpoint show", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint show \n") } + if err := fs.Parse(subArgs); err != nil { + return err + } + if fs.NArg() == 0 { + return &api.ErrConfig{Message: "endpoint ID is required"} + } + return endpoint.RunShow(ctx, client, printer, fs.Arg(0)) + + case "enable": + fs := flag.NewFlagSet("endpoint enable", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint enable \n") } + if err := fs.Parse(subArgs); err != nil { + return err + } + if fs.NArg() == 0 { + return &api.ErrConfig{Message: "endpoint ID is required"} + } + return endpoint.RunEnable(ctx, client, printer, fs.Arg(0)) + + case "disable": + fs := flag.NewFlagSet("endpoint disable", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint disable \n") } + if err := fs.Parse(subArgs); err != nil { + return err + } + if fs.NArg() == 0 { + return &api.ErrConfig{Message: "endpoint ID is required"} + } + return endpoint.RunDisable(ctx, client, printer, fs.Arg(0)) + + case "delete": + fs := flag.NewFlagSet("endpoint delete", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint delete \n") } + if err := fs.Parse(subArgs); err != nil { + return err + } + if fs.NArg() == 0 { + return &api.ErrConfig{Message: "endpoint ID is required"} + } + return endpoint.RunDelete(ctx, client, printer, fs.Arg(0)) + + case "region": + return runEndpointRegion(ctx, client, printer, subArgs) + + default: + return fmt.Errorf("unknown endpoint subcommand %q — run 'krakenkey endpoint --help'", sub) + } +} + +func runEndpointRegion(ctx context.Context, client *api.Client, printer *output.Printer, args []string) error { + if len(args) == 0 || args[0] == "--help" || args[0] == "-h" { + fmt.Print("Usage: krakenkey endpoint region \n") + return nil + } + + sub := args[0] + subArgs := args[1:] + + switch sub { + case "add": + if len(subArgs) < 2 { + return &api.ErrConfig{Message: "endpoint ID and region are required"} + } + return endpoint.RunAddRegion(ctx, client, printer, subArgs[0], subArgs[1]) + + case "remove": + if len(subArgs) < 2 { + return &api.ErrConfig{Message: "endpoint ID and region are required"} + } + return endpoint.RunRemoveRegion(ctx, client, printer, subArgs[0], subArgs[1]) + + default: + return fmt.Errorf("unknown region subcommand %q — use 'add' or 'remove'", sub) + } +} + // ── usage strings ───────────────────────────────────────────────────────────── func printUsage() { @@ -648,6 +778,7 @@ Commands: auth Manage authentication and API keys cert Certificate lifecycle management domain Domain registration and verification + endpoint Endpoint monitoring management account Account and subscription info version Print version and exit @@ -735,3 +866,25 @@ Examples: krakenkey cert download 42 --out ./example.crt krakenkey cert update 42 --auto-renew=true ` + +const endpointUsage = `Manage monitored endpoints. + +Usage: + krakenkey endpoint [flags] + +Subcommands: + add Add a monitored endpoint + list List all endpoints + show Show endpoint details + enable Enable a disabled endpoint + disable Disable an endpoint + delete Delete an endpoint + region add Add a hosted probe region + region remove Remove a hosted probe region + +Examples: + krakenkey endpoint add example.com --port 443 --label "Production" + krakenkey endpoint list + krakenkey endpoint disable + krakenkey endpoint region add us-east-1 +` diff --git a/internal/api/client.go b/internal/api/client.go index 2fecf57..d765ff1 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -249,6 +249,64 @@ func (c *Client) DeleteAPIKey(ctx context.Context, id string) error { return c.do(ctx, http.MethodDelete, "/auth/api-keys/"+id, nil, nil) } +// Endpoint methods + +func (c *Client) CreateEndpoint(ctx context.Context, host string, port int, sni, label *string) (*Endpoint, error) { + body := map[string]any{"host": host, "port": port} + if sni != nil { + body["sni"] = *sni + } + if label != nil { + body["label"] = *label + } + var ep Endpoint + if err := c.do(ctx, http.MethodPost, "/endpoints", body, &ep); err != nil { + return nil, err + } + return &ep, nil +} + +func (c *Client) ListEndpoints(ctx context.Context) ([]Endpoint, error) { + var endpoints []Endpoint + if err := c.do(ctx, http.MethodGet, "/endpoints", nil, &endpoints); err != nil { + return nil, err + } + return endpoints, nil +} + +func (c *Client) GetEndpoint(ctx context.Context, id string) (*Endpoint, error) { + var ep Endpoint + if err := c.do(ctx, http.MethodGet, "/endpoints/"+id, nil, &ep); err != nil { + return nil, err + } + return &ep, nil +} + +func (c *Client) UpdateEndpoint(ctx context.Context, id string, updates map[string]any) (*Endpoint, error) { + var ep Endpoint + if err := c.do(ctx, http.MethodPatch, "/endpoints/"+id, updates, &ep); err != nil { + return nil, err + } + return &ep, nil +} + +func (c *Client) DeleteEndpoint(ctx context.Context, id string) error { + return c.do(ctx, http.MethodDelete, "/endpoints/"+id, nil, nil) +} + +func (c *Client) AddEndpointRegion(ctx context.Context, id, region string) (*EndpointHostedRegion, error) { + body := map[string]string{"region": region} + var ehr EndpointHostedRegion + if err := c.do(ctx, http.MethodPost, "/endpoints/"+id+"/regions", body, &ehr); err != nil { + return nil, err + } + return &ehr, nil +} + +func (c *Client) RemoveEndpointRegion(ctx context.Context, id, region string) error { + return c.do(ctx, http.MethodDelete, "/endpoints/"+id+"/regions/"+region, nil, nil) +} + // Billing methods func (c *Client) GetSubscription(ctx context.Context) (*Subscription, error) { diff --git a/internal/api/types.go b/internal/api/types.go index 6abbe2b..1217e0c 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -131,6 +131,28 @@ type Subscription struct { CreatedAt time.Time `json:"createdAt"` } +// Endpoint represents a monitored endpoint. +type Endpoint struct { + ID string `json:"id"` + UserID string `json:"userId"` + Host string `json:"host"` + Port int `json:"port"` + SNI *string `json:"sni"` + Label *string `json:"label"` + IsActive bool `json:"isActive"` + HostedRegions []EndpointHostedRegion `json:"hostedRegions"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// EndpointHostedRegion links an endpoint to a hosted probe region. +type EndpointHostedRegion struct { + ID string `json:"id"` + EndpointID string `json:"endpointId"` + Region string `json:"region"` + CreatedAt time.Time `json:"createdAt"` +} + // APIError is the error shape returned by the KrakenKey API. type APIError struct { StatusCode int `json:"statusCode"` diff --git a/internal/endpoint/endpoint.go b/internal/endpoint/endpoint.go new file mode 100644 index 0000000..b762c0f --- /dev/null +++ b/internal/endpoint/endpoint.go @@ -0,0 +1,156 @@ +// Package endpoint implements the `krakenkey endpoint` subcommands. +package endpoint + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/krakenkey/cli/internal/api" + "github.com/krakenkey/cli/internal/output" +) + +// RunAdd creates a new monitored endpoint. +func RunAdd(ctx context.Context, client *api.Client, printer *output.Printer, host string, port int, sni, label *string) error { + ep, err := client.CreateEndpoint(ctx, host, port, sni, label) + if err != nil { + return err + } + + printer.JSON(ep) + printer.Success("Endpoint added: %s:%d", ep.Host, ep.Port) + if ep.Label != nil && *ep.Label != "" { + printer.Info("Label: %s", *ep.Label) + } + printer.Info("Run `krakenkey endpoint list` to see all monitored endpoints") + return nil +} + +// RunList lists all monitored endpoints. +func RunList(ctx context.Context, client *api.Client, printer *output.Printer) error { + endpoints, err := client.ListEndpoints(ctx) + if err != nil { + return err + } + + printer.JSON(endpoints) + + if len(endpoints) == 0 { + printer.Info("No endpoints registered") + return nil + } + + headers := []string{"ID", "Host", "Port", "Label", "Active", "Regions", "Created"} + rows := make([][]string, len(endpoints)) + for i, ep := range endpoints { + active := "yes" + if !ep.IsActive { + active = "no" + } + label := "-" + if ep.Label != nil && *ep.Label != "" { + label = *ep.Label + } + regions := "-" + if len(ep.HostedRegions) > 0 { + regionNames := make([]string, len(ep.HostedRegions)) + for j, r := range ep.HostedRegions { + regionNames[j] = r.Region + } + regions = strings.Join(regionNames, ", ") + } + rows[i] = []string{ + ep.ID, + ep.Host, + fmt.Sprintf("%d", ep.Port), + label, + active, + regions, + ep.CreatedAt.Format(time.RFC3339), + } + } + printer.Table(headers, rows) + return nil +} + +// RunShow prints full details for an endpoint. +func RunShow(ctx context.Context, client *api.Client, printer *output.Printer, id string) error { + ep, err := client.GetEndpoint(ctx, id) + if err != nil { + return err + } + + printer.JSON(ep) + printer.Println("ID: %s", ep.ID) + printer.Println("Host: %s", ep.Host) + printer.Println("Port: %d", ep.Port) + if ep.SNI != nil { + printer.Println("SNI: %s", *ep.SNI) + } + if ep.Label != nil { + printer.Println("Label: %s", *ep.Label) + } + printer.Println("Active: %v", ep.IsActive) + if len(ep.HostedRegions) > 0 { + regionNames := make([]string, len(ep.HostedRegions)) + for i, r := range ep.HostedRegions { + regionNames[i] = r.Region + } + printer.Println("Regions: %s", strings.Join(regionNames, ", ")) + } + printer.Println("Created: %s", ep.CreatedAt.Format(time.RFC3339)) + return nil +} + +// RunUpdate updates an endpoint's mutable fields. +func RunUpdate(ctx context.Context, client *api.Client, printer *output.Printer, id string, updates map[string]any) error { + ep, err := client.UpdateEndpoint(ctx, id, updates) + if err != nil { + return err + } + + printer.JSON(ep) + printer.Success("Endpoint %s:%d updated", ep.Host, ep.Port) + return nil +} + +// RunDelete deletes an endpoint by ID. +func RunDelete(ctx context.Context, client *api.Client, printer *output.Printer, id string) error { + if err := client.DeleteEndpoint(ctx, id); err != nil { + return err + } + printer.Success("Endpoint %s deleted", id) + return nil +} + +// RunEnable enables a disabled endpoint. +func RunEnable(ctx context.Context, client *api.Client, printer *output.Printer, id string) error { + return RunUpdate(ctx, client, printer, id, map[string]any{"isActive": true}) +} + +// RunDisable disables an endpoint. +func RunDisable(ctx context.Context, client *api.Client, printer *output.Printer, id string) error { + return RunUpdate(ctx, client, printer, id, map[string]any{"isActive": false}) +} + +// RunAddRegion adds a hosted probe region to an endpoint. +func RunAddRegion(ctx context.Context, client *api.Client, printer *output.Printer, id, region string) error { + ehr, err := client.AddEndpointRegion(ctx, id, region) + if err != nil { + return err + } + + printer.JSON(ehr) + printer.Success("Region %s added to endpoint %s", ehr.Region, id) + return nil +} + +// RunRemoveRegion removes a hosted probe region from an endpoint. +func RunRemoveRegion(ctx context.Context, client *api.Client, printer *output.Printer, id, region string) error { + if err := client.RemoveEndpointRegion(ctx, id, region); err != nil { + return err + } + printer.Success("Region %s removed from endpoint %s", region, id) + return nil +} From d58b2d205bec9675a49d1a6d5f64415d88b5f381 Mon Sep 17 00:00:00 2001 From: krakenhavoc Date: Wed, 25 Mar 2026 16:29:43 +0000 Subject: [PATCH 2/4] feat: probe assignment configuration --- cmd/krakenkey/main.go | 31 +++++++++++++--------- internal/api/client.go | 13 +++++++++- internal/api/types.go | 32 ++++++++++++++++++++--- internal/endpoint/endpoint.go | 48 +++++++++++++++++++++++++++++++++-- 4 files changed, 105 insertions(+), 19 deletions(-) diff --git a/cmd/krakenkey/main.go b/cmd/krakenkey/main.go index 06c42ec..a46e165 100644 --- a/cmd/krakenkey/main.go +++ b/cmd/krakenkey/main.go @@ -655,15 +655,17 @@ func runEndpoint(ctx context.Context, client *api.Client, printer *output.Printe fs := flag.NewFlagSet("endpoint add", flag.ContinueOnError) fs.SetOutput(os.Stderr) var ( - port int - sniFlag string + port int + sniFlag string labelFlag string + probes stringsFlag ) fs.IntVar(&port, "port", 443, "Port to monitor (default: 443)") fs.StringVar(&sniFlag, "sni", "", "SNI override (optional)") fs.StringVar(&labelFlag, "label", "", "Label (optional)") + fs.Var(&probes, "probe", "Connected probe ID to assign (repeat for multiple)") fs.Usage = func() { - fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint add [--port 443] [--sni host] [--label name]\n") + fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint add [--port 443] [--sni host] [--label name] [--probe id]\n") } if err := fs.Parse(subArgs); err != nil { return err @@ -678,7 +680,10 @@ func runEndpoint(ctx context.Context, client *api.Client, printer *output.Printe if labelFlag != "" { label = &labelFlag } - return endpoint.RunAdd(ctx, client, printer, fs.Arg(0), port, sni, label) + return endpoint.RunAdd(ctx, client, printer, fs.Arg(0), port, sni, label, []string(probes)) + + case "probes": + return endpoint.RunListProbes(ctx, client, printer) case "list": return endpoint.RunList(ctx, client, printer) @@ -873,17 +878,19 @@ Usage: krakenkey endpoint [flags] Subcommands: - add Add a monitored endpoint - list List all endpoints - show Show endpoint details - enable Enable a disabled endpoint - disable Disable an endpoint - delete Delete an endpoint - region add Add a hosted probe region + add Add a monitored endpoint + list List all endpoints + show Show endpoint details + probes List your connected probes available for assignment + enable Enable a disabled endpoint + disable Disable an endpoint + delete Delete an endpoint + region add Add a hosted probe region region remove Remove a hosted probe region Examples: - krakenkey endpoint add example.com --port 443 --label "Production" + krakenkey endpoint probes + krakenkey endpoint add example.com --port 443 --label "Production" --probe krakenkey endpoint list krakenkey endpoint disable krakenkey endpoint region add us-east-1 diff --git a/internal/api/client.go b/internal/api/client.go index d765ff1..33d2d28 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -251,7 +251,7 @@ func (c *Client) DeleteAPIKey(ctx context.Context, id string) error { // Endpoint methods -func (c *Client) CreateEndpoint(ctx context.Context, host string, port int, sni, label *string) (*Endpoint, error) { +func (c *Client) CreateEndpoint(ctx context.Context, host string, port int, sni, label *string, probeIds []string) (*Endpoint, error) { body := map[string]any{"host": host, "port": port} if sni != nil { body["sni"] = *sni @@ -259,6 +259,9 @@ func (c *Client) CreateEndpoint(ctx context.Context, host string, port int, sni, if label != nil { body["label"] = *label } + if len(probeIds) > 0 { + body["probeIds"] = probeIds + } var ep Endpoint if err := c.do(ctx, http.MethodPost, "/endpoints", body, &ep); err != nil { return nil, err @@ -266,6 +269,14 @@ func (c *Client) CreateEndpoint(ctx context.Context, host string, port int, sni, return &ep, nil } +func (c *Client) ListUserProbes(ctx context.Context) ([]Probe, error) { + var probes []Probe + if err := c.do(ctx, http.MethodGet, "/endpoints/probes/mine", nil, &probes); err != nil { + return nil, err + } + return probes, nil +} + func (c *Client) ListEndpoints(ctx context.Context) ([]Endpoint, error) { var endpoints []Endpoint if err := c.do(ctx, http.MethodGet, "/endpoints", nil, &endpoints); err != nil { diff --git a/internal/api/types.go b/internal/api/types.go index 1217e0c..d75a637 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -139,10 +139,11 @@ type Endpoint struct { Port int `json:"port"` SNI *string `json:"sni"` Label *string `json:"label"` - IsActive bool `json:"isActive"` - HostedRegions []EndpointHostedRegion `json:"hostedRegions"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + IsActive bool `json:"isActive"` + HostedRegions []EndpointHostedRegion `json:"hostedRegions"` + ProbeAssignments []EndpointProbeAssignment `json:"probeAssignments"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } // EndpointHostedRegion links an endpoint to a hosted probe region. @@ -153,6 +154,29 @@ type EndpointHostedRegion struct { CreatedAt time.Time `json:"createdAt"` } +// Probe represents a registered probe (connected or hosted). +type Probe struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Mode string `json:"mode"` + Region *string `json:"region"` + OS string `json:"os"` + Arch string `json:"arch"` + Status string `json:"status"` + LastSeenAt *time.Time `json:"lastSeenAt"` + CreatedAt time.Time `json:"createdAt"` +} + +// EndpointProbeAssignment links an endpoint to a connected probe. +type EndpointProbeAssignment struct { + ID string `json:"id"` + EndpointID string `json:"endpointId"` + ProbeID string `json:"probeId"` + Probe *Probe `json:"probe"` + CreatedAt time.Time `json:"createdAt"` +} + // APIError is the error shape returned by the KrakenKey API. type APIError struct { StatusCode int `json:"statusCode"` diff --git a/internal/endpoint/endpoint.go b/internal/endpoint/endpoint.go index b762c0f..1049a49 100644 --- a/internal/endpoint/endpoint.go +++ b/internal/endpoint/endpoint.go @@ -12,8 +12,8 @@ import ( ) // RunAdd creates a new monitored endpoint. -func RunAdd(ctx context.Context, client *api.Client, printer *output.Printer, host string, port int, sni, label *string) error { - ep, err := client.CreateEndpoint(ctx, host, port, sni, label) +func RunAdd(ctx context.Context, client *api.Client, printer *output.Printer, host string, port int, sni, label *string, probeIds []string) error { + ep, err := client.CreateEndpoint(ctx, host, port, sni, label, probeIds) if err != nil { return err } @@ -23,10 +23,54 @@ func RunAdd(ctx context.Context, client *api.Client, printer *output.Printer, ho if ep.Label != nil && *ep.Label != "" { printer.Info("Label: %s", *ep.Label) } + if len(ep.ProbeAssignments) > 0 { + names := make([]string, len(ep.ProbeAssignments)) + for i, a := range ep.ProbeAssignments { + if a.Probe != nil { + names[i] = a.Probe.Name + } else { + names[i] = a.ProbeID + } + } + printer.Info("Assigned probes: %s", strings.Join(names, ", ")) + } printer.Info("Run `krakenkey endpoint list` to see all monitored endpoints") return nil } +// RunListProbes lists the user's connected probes available for assignment. +func RunListProbes(ctx context.Context, client *api.Client, printer *output.Printer) error { + probes, err := client.ListUserProbes(ctx) + if err != nil { + return err + } + + printer.JSON(probes) + + if len(probes) == 0 { + printer.Info("No connected probes registered") + printer.Println("") + printer.Println("Set up a probe with KK_PROBE_MODE=connected and your API key to get started") + return nil + } + + headers := []string{"ID", "Name", "Region", "Status", "Last Seen"} + rows := make([][]string, len(probes)) + for i, p := range probes { + region := "-" + if p.Region != nil { + region = *p.Region + } + lastSeen := "-" + if p.LastSeenAt != nil { + lastSeen = p.LastSeenAt.Format(time.RFC3339) + } + rows[i] = []string{p.ID, p.Name, region, p.Status, lastSeen} + } + printer.Table(headers, rows) + return nil +} + // RunList lists all monitored endpoints. func RunList(ctx context.Context, client *api.Client, printer *output.Printer) error { endpoints, err := client.ListEndpoints(ctx) From d65f35eda6d34a252a5a89085e4ae67e99ae940e Mon Sep 17 00:00:00 2001 From: krakenhavoc Date: Thu, 26 Mar 2026 20:33:56 +0000 Subject: [PATCH 3/4] feat: support standalone, connected, hosted probes --- cmd/krakenkey/main.go | 68 +++++++++++++++++++++++++++++------ internal/api/client.go | 21 +++++++++++ internal/endpoint/endpoint.go | 60 ++++++++++++++++++++++++++++++- 3 files changed, 137 insertions(+), 12 deletions(-) diff --git a/cmd/krakenkey/main.go b/cmd/krakenkey/main.go index a46e165..44e948b 100644 --- a/cmd/krakenkey/main.go +++ b/cmd/krakenkey/main.go @@ -736,9 +736,24 @@ func runEndpoint(ctx context.Context, client *api.Client, printer *output.Printe } return endpoint.RunDelete(ctx, client, printer, fs.Arg(0)) + case "scan": + fs := flag.NewFlagSet("endpoint scan", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint scan \n") } + if err := fs.Parse(subArgs); err != nil { + return err + } + if fs.NArg() == 0 { + return &api.ErrConfig{Message: "endpoint ID is required"} + } + return endpoint.RunScan(ctx, client, printer, fs.Arg(0)) + case "region": return runEndpointRegion(ctx, client, printer, subArgs) + case "probe": + return runEndpointProbe(ctx, client, printer, subArgs) + default: return fmt.Errorf("unknown endpoint subcommand %q — run 'krakenkey endpoint --help'", sub) } @@ -771,6 +786,33 @@ func runEndpointRegion(ctx context.Context, client *api.Client, printer *output. } } +func runEndpointProbe(ctx context.Context, client *api.Client, printer *output.Printer, args []string) error { + if len(args) == 0 || args[0] == "--help" || args[0] == "-h" { + fmt.Print("Usage: krakenkey endpoint probe \n") + return nil + } + + sub := args[0] + subArgs := args[1:] + + switch sub { + case "add": + if len(subArgs) < 2 { + return &api.ErrConfig{Message: "endpoint ID and probe ID are required"} + } + return endpoint.RunAssignProbe(ctx, client, printer, subArgs[0], subArgs[1]) + + case "remove": + if len(subArgs) < 2 { + return &api.ErrConfig{Message: "endpoint ID and probe ID are required"} + } + return endpoint.RunUnassignProbe(ctx, client, printer, subArgs[0], subArgs[1]) + + default: + return fmt.Errorf("unknown probe subcommand %q — use 'add' or 'remove'", sub) + } +} + // ── usage strings ───────────────────────────────────────────────────────────── func printUsage() { @@ -878,20 +920,24 @@ Usage: krakenkey endpoint [flags] Subcommands: - add Add a monitored endpoint - list List all endpoints - show Show endpoint details - probes List your connected probes available for assignment - enable Enable a disabled endpoint - disable Disable an endpoint - delete Delete an endpoint - region add Add a hosted probe region - region remove Remove a hosted probe region + add Add a monitored endpoint + list List all endpoints + show Show endpoint details + scan Request an on-demand scan + probes List your connected probes + enable Enable a disabled endpoint + disable Disable an endpoint + delete Delete an endpoint + probe add Assign a connected probe + probe remove Remove a connected probe + region add Add a hosted probe region + region remove Remove a hosted probe region Examples: krakenkey endpoint probes krakenkey endpoint add example.com --port 443 --label "Production" --probe - krakenkey endpoint list - krakenkey endpoint disable + krakenkey endpoint scan + krakenkey endpoint probe add krakenkey endpoint region add us-east-1 + krakenkey endpoint list ` diff --git a/internal/api/client.go b/internal/api/client.go index 33d2d28..3374e41 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -318,6 +318,27 @@ func (c *Client) RemoveEndpointRegion(ctx context.Context, id, region string) er return c.do(ctx, http.MethodDelete, "/endpoints/"+id+"/regions/"+region, nil, nil) } +func (c *Client) AssignProbes(ctx context.Context, id string, probeIds []string) ([]EndpointProbeAssignment, error) { + body := map[string]any{"probeIds": probeIds} + var assignments []EndpointProbeAssignment + if err := c.do(ctx, http.MethodPost, "/endpoints/"+id+"/probes", body, &assignments); err != nil { + return nil, err + } + return assignments, nil +} + +func (c *Client) UnassignProbe(ctx context.Context, endpointID, probeID string) error { + return c.do(ctx, http.MethodDelete, "/endpoints/"+endpointID+"/probes/"+probeID, nil, nil) +} + +func (c *Client) RequestScan(ctx context.Context, id string) (*Endpoint, error) { + var ep Endpoint + if err := c.do(ctx, http.MethodPost, "/endpoints/"+id+"/scan", nil, &ep); err != nil { + return nil, err + } + return &ep, nil +} + // Billing methods func (c *Client) GetSubscription(ctx context.Context) (*Subscription, error) { diff --git a/internal/endpoint/endpoint.go b/internal/endpoint/endpoint.go index 1049a49..935d7f8 100644 --- a/internal/endpoint/endpoint.go +++ b/internal/endpoint/endpoint.go @@ -85,7 +85,7 @@ func RunList(ctx context.Context, client *api.Client, printer *output.Printer) e return nil } - headers := []string{"ID", "Host", "Port", "Label", "Active", "Regions", "Created"} + headers := []string{"ID", "Host", "Port", "Label", "Active", "Probes", "Regions", "Created"} rows := make([][]string, len(endpoints)) for i, ep := range endpoints { active := "yes" @@ -96,6 +96,18 @@ func RunList(ctx context.Context, client *api.Client, printer *output.Printer) e if ep.Label != nil && *ep.Label != "" { label = *ep.Label } + probes := "-" + if len(ep.ProbeAssignments) > 0 { + probeNames := make([]string, len(ep.ProbeAssignments)) + for j, a := range ep.ProbeAssignments { + if a.Probe != nil { + probeNames[j] = a.Probe.Name + } else { + probeNames[j] = a.ProbeID[:8] + } + } + probes = strings.Join(probeNames, ", ") + } regions := "-" if len(ep.HostedRegions) > 0 { regionNames := make([]string, len(ep.HostedRegions)) @@ -110,6 +122,7 @@ func RunList(ctx context.Context, client *api.Client, printer *output.Printer) e fmt.Sprintf("%d", ep.Port), label, active, + probes, regions, ep.CreatedAt.Format(time.RFC3339), } @@ -143,6 +156,17 @@ func RunShow(ctx context.Context, client *api.Client, printer *output.Printer, i } printer.Println("Regions: %s", strings.Join(regionNames, ", ")) } + if len(ep.ProbeAssignments) > 0 { + probeNames := make([]string, len(ep.ProbeAssignments)) + for i, a := range ep.ProbeAssignments { + if a.Probe != nil { + probeNames[i] = fmt.Sprintf("%s (%s)", a.Probe.Name, a.ProbeID) + } else { + probeNames[i] = a.ProbeID + } + } + printer.Println("Probes: %s", strings.Join(probeNames, ", ")) + } printer.Println("Created: %s", ep.CreatedAt.Format(time.RFC3339)) return nil } @@ -198,3 +222,37 @@ func RunRemoveRegion(ctx context.Context, client *api.Client, printer *output.Pr printer.Success("Region %s removed from endpoint %s", region, id) return nil } + +// RunAssignProbe assigns a connected probe to an endpoint. +func RunAssignProbe(ctx context.Context, client *api.Client, printer *output.Printer, id, probeID string) error { + assignments, err := client.AssignProbes(ctx, id, []string{probeID}) + if err != nil { + return err + } + + printer.JSON(assignments) + printer.Success("Probe %s assigned to endpoint %s", probeID, id) + return nil +} + +// RunUnassignProbe removes a connected probe from an endpoint. +func RunUnassignProbe(ctx context.Context, client *api.Client, printer *output.Printer, id, probeID string) error { + if err := client.UnassignProbe(ctx, id, probeID); err != nil { + return err + } + printer.Success("Probe %s removed from endpoint %s", probeID, id) + return nil +} + +// RunScan requests an on-demand scan of an endpoint. +func RunScan(ctx context.Context, client *api.Client, printer *output.Printer, id string) error { + ep, err := client.RequestScan(ctx, id) + if err != nil { + return err + } + + printer.JSON(ep) + printer.Success("Scan requested for %s:%d", ep.Host, ep.Port) + printer.Info("Results will appear shortly in the dashboard or via `krakenkey endpoint show %s`", id) + return nil +} From 90425b43265e8b61bc46beffc33bafecd244cdb5 Mon Sep 17 00:00:00 2001 From: krakenhavoc Date: Thu, 26 Mar 2026 20:40:42 +0000 Subject: [PATCH 4/4] fix: pre-commit changes --- .pre-commit-config.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a5f6c2c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: local + hooks: + - id: go-vet + name: go vet + entry: go vet ./... + language: system + pass_filenames: false + types: [go] + + - id: go-test + name: go test + entry: go test ./... -race -count=1 + language: system + pass_filenames: false + types: [go]