Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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]
206 changes: 206 additions & 0 deletions cmd/krakenkey/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -636,6 +639,180 @@ 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
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 <host> [--port 443] [--sni host] [--label name] [--probe id]\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, []string(probes))

case "probes":
return endpoint.RunListProbes(ctx, client, printer)

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 <id>\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 <id>\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 <id>\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 <id>\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 "scan":
fs := flag.NewFlagSet("endpoint scan", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
fs.Usage = func() { fmt.Fprint(os.Stderr, "Usage: krakenkey endpoint scan <id>\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)
}
}

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 <add|remove> <endpoint-id> <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)
}
}

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 <add|remove> <endpoint-id> <probe-id>\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() {
Expand All @@ -648,6 +825,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

Expand Down Expand Up @@ -735,3 +913,31 @@ Examples:
krakenkey cert download 42 --out ./example.crt
krakenkey cert update 42 --auto-renew=true
`

const endpointUsage = `Manage monitored endpoints.

Usage:
krakenkey endpoint <subcommand> [flags]

Subcommands:
add <host> Add a monitored endpoint
list List all endpoints
show <id> Show endpoint details
scan <id> Request an on-demand scan
probes List your connected probes
enable <id> Enable a disabled endpoint
disable <id> Disable an endpoint
delete <id> Delete an endpoint
probe add <id> <probe-id> Assign a connected probe
probe remove <id> <probe-id> Remove a connected probe
region add <id> <region> Add a hosted probe region
region remove <id> <region> Remove a hosted probe region

Examples:
krakenkey endpoint probes
krakenkey endpoint add example.com --port 443 --label "Production" --probe <probe-id>
krakenkey endpoint scan <id>
krakenkey endpoint probe add <id> <probe-id>
krakenkey endpoint region add <id> us-east-1
krakenkey endpoint list
`
90 changes: 90 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,96 @@ 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, probeIds []string) (*Endpoint, error) {
body := map[string]any{"host": host, "port": port}
if sni != nil {
body["sni"] = *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
}
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 {
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)
}

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) {
Expand Down
Loading