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
42 changes: 42 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
##################################
# KrakenKey Probe Configuration #
##################################

# Mode: "standalone" or "connected"
# standalone - fully local, no API communication, results logged only
# connected - endpoints managed in KrakenKey dashboard, fetched from API
KK_PROBE_MODE=standalone

# Probe display name (shown in health endpoint / dashboard)
KK_PROBE_NAME=my-probe

# --- Standalone mode ---
# Comma-separated list of host:port to monitor (standalone only)
KK_PROBE_ENDPOINTS=example.com:443,api.example.com:443

# --- Connected mode ---
# Your KrakenKey user API key (kk_ prefix, required for connected mode)
# KK_PROBE_API_KEY=kk_your_api_key_here

# Probe ID from KrakenKey dashboard (required for connected mode)
# KK_PROBE_ID=your-probe-uuid

# API base URL (default: https://api.krakenkey.io)
# KK_PROBE_API_URL=https://api.krakenkey.io

# --- Shared options ---

# Scan interval (default: 60m, min: 1m, max: 24h)
KK_PROBE_INTERVAL=30m

# Geographic region label (optional, for organizing probes)
# KK_PROBE_REGION=us-east-1

# Host port for health/metrics endpoint (default: 8080)
# KK_PROBE_HOST_PORT=8080

# Log level: debug, info, warn, error (default: info)
KK_PROBE_LOG_LEVEL=info

# Log format: json or text (default: json)
KK_PROBE_LOG_FORMAT=json
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Binary
probe
*.exe

# Environment (contains API keys)
.env

# State file
state.json

# IDE
.idea/
.vscode/

# OS
.DS_Store
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X main.version=${VERSION}" -o /probe ./cmd/probe
RUN mkdir -p /var/lib/krakenkey-probe && chown 65532:65532 /var/lib/krakenkey-probe

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /probe /probe
COPY probe.example.yaml /etc/krakenkey-probe/probe.yaml
COPY --from=build --chown=nonroot:nonroot /var/lib/krakenkey-probe /var/lib/krakenkey-probe
VOLUME ["/var/lib/krakenkey-probe"]
EXPOSE 8080
ENTRYPOINT ["/probe"]
Expand Down
52 changes: 37 additions & 15 deletions cmd/probe/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"runtime"
Expand All @@ -22,13 +23,18 @@ var version = "dev"
func main() {
cfgPath := flag.String("config", "", "path to probe.yaml config file")
showVersion := flag.Bool("version", false, "print version and exit")
healthcheck := flag.Bool("healthcheck", false, "check health endpoint and exit")
flag.Parse()

if *showVersion {
fmt.Printf("krakenkey-probe %s (%s/%s)\n", version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}

if *healthcheck {
os.Exit(runHealthcheck())
}

cfg, err := config.Load(*cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
Expand All @@ -52,25 +58,29 @@ func main() {
"interval", cfg.Probe.Interval.String(),
)

rep := reporter.New(cfg.API.URL, cfg.API.Key, version, runtime.GOOS, runtime.GOARCH)

// Register with the API
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

regInfo := reporter.ProbeInfo{
ProbeID: probeID,
Name: cfg.Probe.Name,
Version: version,
Mode: cfg.Probe.Mode,
Region: cfg.Probe.Region,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}
if err := rep.Register(ctx, regInfo); err != nil {
logger.Warn("failed to register with API (will retry on next cycle)", "error", err)
var rep *reporter.Reporter
if cfg.Probe.Mode != "standalone" {
rep = reporter.New(cfg.API.URL, cfg.API.Key, version, runtime.GOOS, runtime.GOARCH)

regInfo := reporter.ProbeInfo{
ProbeID: probeID,
Name: cfg.Probe.Name,
Version: version,
Mode: cfg.Probe.Mode,
Region: cfg.Probe.Region,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}
if err := rep.Register(ctx, regInfo); err != nil {
logger.Warn("failed to register with API (will retry on next cycle)", "error", err)
} else {
logger.Info("registered with API")
}
} else {
logger.Info("registered with API")
logger.Info("standalone mode: no API communication")
}

// Start health server
Expand Down Expand Up @@ -101,6 +111,18 @@ func main() {
logger.Info("krakenkey-probe stopped")
}

func runHealthcheck() int {
resp, err := http.Get("http://localhost:8080/healthz")
if err != nil {
return 1
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return 0
}
return 1
}

func setupLogger(level, format string) *slog.Logger {
var lvl slog.Level
switch level {
Expand Down
22 changes: 16 additions & 6 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
services:
# Standalone mode: endpoints defined in .env, results reported to API
# Connected mode: endpoints fetched from API, authenticated with user API key
#
# Usage:
# cp .env.example .env # edit with your values
# docker compose up -d
krakenkey-probe:
build: .
image: ghcr.io/krakenkey/probe:latest
container_name: krakenkey-probe
restart: unless-stopped
environment:
KK_PROBE_API_KEY: "kk_your_api_key_here"
KK_PROBE_NAME: "my-probe"
KK_PROBE_ENDPOINTS: "example.com:443,api.example.com:443,internal.corp:8443"
KK_PROBE_INTERVAL: "30m"
env_file:
- .env
volumes:
- probe_state:/var/lib/krakenkey-probe
ports:
- "8080:8080"
- "${KK_PROBE_HOST_PORT:-8080}:8080"
healthcheck:
test: ["CMD", "/probe", "--healthcheck"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s

volumes:
probe_state:
31 changes: 20 additions & 11 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func defaults() *Config {
URL: "https://api.krakenkey.io",
},
Probe: ProbeConfig{
Mode: "self-hosted",
Mode: "standalone",
RawInterval: "60m",
RawTimeout: "10s",
StateFile: "/var/lib/krakenkey-probe/state.json",
Expand Down Expand Up @@ -186,17 +186,23 @@ func parseDurations(cfg *Config) error {
}

func validate(cfg *Config) error {
if cfg.API.Key == "" {
return fmt.Errorf("api.key is required")
}

switch cfg.Probe.Mode {
case "self-hosted":
if !strings.HasPrefix(cfg.API.Key, "kk_") {
return fmt.Errorf("api.key must start with 'kk_' for self-hosted mode")
}
case "standalone":
if len(cfg.Endpoints) == 0 {
return fmt.Errorf("at least one endpoint is required for self-hosted mode")
return fmt.Errorf("at least one endpoint is required for standalone mode")
}
if cfg.Probe.Interval < time.Minute {
return fmt.Errorf("interval must be at least 1m, got %s", cfg.Probe.Interval)
}
if cfg.Probe.Interval > 24*time.Hour {
return fmt.Errorf("interval must be at most 24h, got %s", cfg.Probe.Interval)
}
case "connected":
if cfg.API.Key == "" {
return fmt.Errorf("api.key is required for connected mode")
}
if !strings.HasPrefix(cfg.API.Key, "kk_") || strings.HasPrefix(cfg.API.Key, "kk_svc_") {
return fmt.Errorf("api.key must be a user API key (kk_ prefix, not kk_svc_) for connected mode")
}
if cfg.Probe.Interval < time.Minute {
return fmt.Errorf("interval must be at least 1m, got %s", cfg.Probe.Interval)
Expand All @@ -205,14 +211,17 @@ func validate(cfg *Config) error {
return fmt.Errorf("interval must be at most 24h, got %s", cfg.Probe.Interval)
}
case "hosted":
if cfg.API.Key == "" {
return fmt.Errorf("api.key is required for hosted mode")
}
if cfg.Probe.Region == "" {
return fmt.Errorf("probe.region is required for hosted mode")
}
if cfg.Probe.ID == "" {
return fmt.Errorf("probe.id is required for hosted mode")
}
default:
return fmt.Errorf("probe.mode must be 'self-hosted' or 'hosted', got %q", cfg.Probe.Mode)
return fmt.Errorf("probe.mode must be 'standalone', 'connected', or 'hosted', got %q", cfg.Probe.Mode)
}

for i, ep := range cfg.Endpoints {
Expand Down
Loading
Loading