Skip to content

Commit d4a5f53

Browse files
authored
feat: add support for probes and endpoints (#3)
* feat: add support for probes and endpoints * feat: probe assignment configuration * feat: support standalone, connected, hosted probes * fix: pre-commit changes
1 parent ac62425 commit d4a5f53

5 files changed

Lines changed: 616 additions & 0 deletions

File tree

.pre-commit-config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
repos:
2+
- repo: local
3+
hooks:
4+
- id: go-vet
5+
name: go vet
6+
entry: go vet ./...
7+
language: system
8+
pass_filenames: false
9+
types: [go]
10+
11+
- id: go-test
12+
name: go test
13+
entry: go test ./... -race -count=1
14+
language: system
15+
pass_filenames: false
16+
types: [go]

cmd/krakenkey/main.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/krakenkey/cli/internal/cert"
1919
"github.com/krakenkey/cli/internal/config"
2020
"github.com/krakenkey/cli/internal/domain"
21+
"github.com/krakenkey/cli/internal/endpoint"
2122
"github.com/krakenkey/cli/internal/output"
2223
)
2324

@@ -113,6 +114,8 @@ func run() int {
113114
cmdErr = runAccount(ctx, client, printer, subArgs)
114115
case "cert":
115116
cmdErr = runCert(ctx, client, printer, cfg, subArgs)
117+
case "endpoint":
118+
cmdErr = runEndpoint(ctx, client, printer, subArgs)
116119
default:
117120
fmt.Fprintf(os.Stderr, "error: unknown command %q — run 'krakenkey help'\n", cmd)
118121
return 1
@@ -636,6 +639,180 @@ func runCert(ctx context.Context, client *api.Client, printer *output.Printer, c
636639
}
637640
}
638641

642+
// ── endpoint ──────────────────────────────────────────────────────────────────
643+
644+
func runEndpoint(ctx context.Context, client *api.Client, printer *output.Printer, args []string) error {
645+
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
646+
fmt.Print(endpointUsage)
647+
return nil
648+
}
649+
650+
sub := args[0]
651+
subArgs := args[1:]
652+
653+
switch sub {
654+
case "add":
655+
fs := flag.NewFlagSet("endpoint add", flag.ContinueOnError)
656+
fs.SetOutput(os.Stderr)
657+
var (
658+
port int
659+
sniFlag string
660+
labelFlag string
661+
probes stringsFlag
662+
)
663+
fs.IntVar(&port, "port", 443, "Port to monitor (default: 443)")
664+
fs.StringVar(&sniFlag, "sni", "", "SNI override (optional)")
665+
fs.StringVar(&labelFlag, "label", "", "Label (optional)")
666+
fs.Var(&probes, "probe", "Connected probe ID to assign (repeat for multiple)")
667+
fs.Usage = func() {
668+
fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint add <host> [--port 443] [--sni host] [--label name] [--probe id]\n")
669+
}
670+
if err := fs.Parse(subArgs); err != nil {
671+
return err
672+
}
673+
if fs.NArg() == 0 {
674+
return &api.ErrConfig{Message: "hostname is required"}
675+
}
676+
var sni, label *string
677+
if sniFlag != "" {
678+
sni = &sniFlag
679+
}
680+
if labelFlag != "" {
681+
label = &labelFlag
682+
}
683+
return endpoint.RunAdd(ctx, client, printer, fs.Arg(0), port, sni, label, []string(probes))
684+
685+
case "probes":
686+
return endpoint.RunListProbes(ctx, client, printer)
687+
688+
case "list":
689+
return endpoint.RunList(ctx, client, printer)
690+
691+
case "show":
692+
fs := flag.NewFlagSet("endpoint show", flag.ContinueOnError)
693+
fs.SetOutput(os.Stderr)
694+
fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint show <id>\n") }
695+
if err := fs.Parse(subArgs); err != nil {
696+
return err
697+
}
698+
if fs.NArg() == 0 {
699+
return &api.ErrConfig{Message: "endpoint ID is required"}
700+
}
701+
return endpoint.RunShow(ctx, client, printer, fs.Arg(0))
702+
703+
case "enable":
704+
fs := flag.NewFlagSet("endpoint enable", flag.ContinueOnError)
705+
fs.SetOutput(os.Stderr)
706+
fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint enable <id>\n") }
707+
if err := fs.Parse(subArgs); err != nil {
708+
return err
709+
}
710+
if fs.NArg() == 0 {
711+
return &api.ErrConfig{Message: "endpoint ID is required"}
712+
}
713+
return endpoint.RunEnable(ctx, client, printer, fs.Arg(0))
714+
715+
case "disable":
716+
fs := flag.NewFlagSet("endpoint disable", flag.ContinueOnError)
717+
fs.SetOutput(os.Stderr)
718+
fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint disable <id>\n") }
719+
if err := fs.Parse(subArgs); err != nil {
720+
return err
721+
}
722+
if fs.NArg() == 0 {
723+
return &api.ErrConfig{Message: "endpoint ID is required"}
724+
}
725+
return endpoint.RunDisable(ctx, client, printer, fs.Arg(0))
726+
727+
case "delete":
728+
fs := flag.NewFlagSet("endpoint delete", flag.ContinueOnError)
729+
fs.SetOutput(os.Stderr)
730+
fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint delete <id>\n") }
731+
if err := fs.Parse(subArgs); err != nil {
732+
return err
733+
}
734+
if fs.NArg() == 0 {
735+
return &api.ErrConfig{Message: "endpoint ID is required"}
736+
}
737+
return endpoint.RunDelete(ctx, client, printer, fs.Arg(0))
738+
739+
case "scan":
740+
fs := flag.NewFlagSet("endpoint scan", flag.ContinueOnError)
741+
fs.SetOutput(os.Stderr)
742+
fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint scan <id>\n") }
743+
if err := fs.Parse(subArgs); err != nil {
744+
return err
745+
}
746+
if fs.NArg() == 0 {
747+
return &api.ErrConfig{Message: "endpoint ID is required"}
748+
}
749+
return endpoint.RunScan(ctx, client, printer, fs.Arg(0))
750+
751+
case "region":
752+
return runEndpointRegion(ctx, client, printer, subArgs)
753+
754+
case "probe":
755+
return runEndpointProbe(ctx, client, printer, subArgs)
756+
757+
default:
758+
return fmt.Errorf("unknown endpoint subcommand %q — run 'krakenkey endpoint --help'", sub)
759+
}
760+
}
761+
762+
func runEndpointRegion(ctx context.Context, client *api.Client, printer *output.Printer, args []string) error {
763+
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
764+
fmt.Print("Usage: krakenkey endpoint region <add|remove> <endpoint-id> <region>\n")
765+
return nil
766+
}
767+
768+
sub := args[0]
769+
subArgs := args[1:]
770+
771+
switch sub {
772+
case "add":
773+
if len(subArgs) < 2 {
774+
return &api.ErrConfig{Message: "endpoint ID and region are required"}
775+
}
776+
return endpoint.RunAddRegion(ctx, client, printer, subArgs[0], subArgs[1])
777+
778+
case "remove":
779+
if len(subArgs) < 2 {
780+
return &api.ErrConfig{Message: "endpoint ID and region are required"}
781+
}
782+
return endpoint.RunRemoveRegion(ctx, client, printer, subArgs[0], subArgs[1])
783+
784+
default:
785+
return fmt.Errorf("unknown region subcommand %q — use 'add' or 'remove'", sub)
786+
}
787+
}
788+
789+
func runEndpointProbe(ctx context.Context, client *api.Client, printer *output.Printer, args []string) error {
790+
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
791+
fmt.Print("Usage: krakenkey endpoint probe <add|remove> <endpoint-id> <probe-id>\n")
792+
return nil
793+
}
794+
795+
sub := args[0]
796+
subArgs := args[1:]
797+
798+
switch sub {
799+
case "add":
800+
if len(subArgs) < 2 {
801+
return &api.ErrConfig{Message: "endpoint ID and probe ID are required"}
802+
}
803+
return endpoint.RunAssignProbe(ctx, client, printer, subArgs[0], subArgs[1])
804+
805+
case "remove":
806+
if len(subArgs) < 2 {
807+
return &api.ErrConfig{Message: "endpoint ID and probe ID are required"}
808+
}
809+
return endpoint.RunUnassignProbe(ctx, client, printer, subArgs[0], subArgs[1])
810+
811+
default:
812+
return fmt.Errorf("unknown probe subcommand %q — use 'add' or 'remove'", sub)
813+
}
814+
}
815+
639816
// ── usage strings ─────────────────────────────────────────────────────────────
640817

641818
func printUsage() {
@@ -648,6 +825,7 @@ Commands:
648825
auth Manage authentication and API keys
649826
cert Certificate lifecycle management
650827
domain Domain registration and verification
828+
endpoint Endpoint monitoring management
651829
account Account and subscription info
652830
version Print version and exit
653831
@@ -735,3 +913,31 @@ Examples:
735913
krakenkey cert download 42 --out ./example.crt
736914
krakenkey cert update 42 --auto-renew=true
737915
`
916+
917+
const endpointUsage = `Manage monitored endpoints.
918+
919+
Usage:
920+
krakenkey endpoint <subcommand> [flags]
921+
922+
Subcommands:
923+
add <host> Add a monitored endpoint
924+
list List all endpoints
925+
show <id> Show endpoint details
926+
scan <id> Request an on-demand scan
927+
probes List your connected probes
928+
enable <id> Enable a disabled endpoint
929+
disable <id> Disable an endpoint
930+
delete <id> Delete an endpoint
931+
probe add <id> <probe-id> Assign a connected probe
932+
probe remove <id> <probe-id> Remove a connected probe
933+
region add <id> <region> Add a hosted probe region
934+
region remove <id> <region> Remove a hosted probe region
935+
936+
Examples:
937+
krakenkey endpoint probes
938+
krakenkey endpoint add example.com --port 443 --label "Production" --probe <probe-id>
939+
krakenkey endpoint scan <id>
940+
krakenkey endpoint probe add <id> <probe-id>
941+
krakenkey endpoint region add <id> us-east-1
942+
krakenkey endpoint list
943+
`

internal/api/client.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,96 @@ func (c *Client) DeleteAPIKey(ctx context.Context, id string) error {
249249
return c.do(ctx, http.MethodDelete, "/auth/api-keys/"+id, nil, nil)
250250
}
251251

252+
// Endpoint methods
253+
254+
func (c *Client) CreateEndpoint(ctx context.Context, host string, port int, sni, label *string, probeIds []string) (*Endpoint, error) {
255+
body := map[string]any{"host": host, "port": port}
256+
if sni != nil {
257+
body["sni"] = *sni
258+
}
259+
if label != nil {
260+
body["label"] = *label
261+
}
262+
if len(probeIds) > 0 {
263+
body["probeIds"] = probeIds
264+
}
265+
var ep Endpoint
266+
if err := c.do(ctx, http.MethodPost, "/endpoints", body, &ep); err != nil {
267+
return nil, err
268+
}
269+
return &ep, nil
270+
}
271+
272+
func (c *Client) ListUserProbes(ctx context.Context) ([]Probe, error) {
273+
var probes []Probe
274+
if err := c.do(ctx, http.MethodGet, "/endpoints/probes/mine", nil, &probes); err != nil {
275+
return nil, err
276+
}
277+
return probes, nil
278+
}
279+
280+
func (c *Client) ListEndpoints(ctx context.Context) ([]Endpoint, error) {
281+
var endpoints []Endpoint
282+
if err := c.do(ctx, http.MethodGet, "/endpoints", nil, &endpoints); err != nil {
283+
return nil, err
284+
}
285+
return endpoints, nil
286+
}
287+
288+
func (c *Client) GetEndpoint(ctx context.Context, id string) (*Endpoint, error) {
289+
var ep Endpoint
290+
if err := c.do(ctx, http.MethodGet, "/endpoints/"+id, nil, &ep); err != nil {
291+
return nil, err
292+
}
293+
return &ep, nil
294+
}
295+
296+
func (c *Client) UpdateEndpoint(ctx context.Context, id string, updates map[string]any) (*Endpoint, error) {
297+
var ep Endpoint
298+
if err := c.do(ctx, http.MethodPatch, "/endpoints/"+id, updates, &ep); err != nil {
299+
return nil, err
300+
}
301+
return &ep, nil
302+
}
303+
304+
func (c *Client) DeleteEndpoint(ctx context.Context, id string) error {
305+
return c.do(ctx, http.MethodDelete, "/endpoints/"+id, nil, nil)
306+
}
307+
308+
func (c *Client) AddEndpointRegion(ctx context.Context, id, region string) (*EndpointHostedRegion, error) {
309+
body := map[string]string{"region": region}
310+
var ehr EndpointHostedRegion
311+
if err := c.do(ctx, http.MethodPost, "/endpoints/"+id+"/regions", body, &ehr); err != nil {
312+
return nil, err
313+
}
314+
return &ehr, nil
315+
}
316+
317+
func (c *Client) RemoveEndpointRegion(ctx context.Context, id, region string) error {
318+
return c.do(ctx, http.MethodDelete, "/endpoints/"+id+"/regions/"+region, nil, nil)
319+
}
320+
321+
func (c *Client) AssignProbes(ctx context.Context, id string, probeIds []string) ([]EndpointProbeAssignment, error) {
322+
body := map[string]any{"probeIds": probeIds}
323+
var assignments []EndpointProbeAssignment
324+
if err := c.do(ctx, http.MethodPost, "/endpoints/"+id+"/probes", body, &assignments); err != nil {
325+
return nil, err
326+
}
327+
return assignments, nil
328+
}
329+
330+
func (c *Client) UnassignProbe(ctx context.Context, endpointID, probeID string) error {
331+
return c.do(ctx, http.MethodDelete, "/endpoints/"+endpointID+"/probes/"+probeID, nil, nil)
332+
}
333+
334+
func (c *Client) RequestScan(ctx context.Context, id string) (*Endpoint, error) {
335+
var ep Endpoint
336+
if err := c.do(ctx, http.MethodPost, "/endpoints/"+id+"/scan", nil, &ep); err != nil {
337+
return nil, err
338+
}
339+
return &ep, nil
340+
}
341+
252342
// Billing methods
253343

254344
func (c *Client) GetSubscription(ctx context.Context) (*Subscription, error) {

0 commit comments

Comments
 (0)