From 7ae7c546c814ae0a752883b37fe9a431f94c816c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:35:11 +0000 Subject: [PATCH 01/13] feat: add krbctl CLI with interactive compose and config commands Co-authored-by: maansaake <15028979+maansaake@users.noreply.github.com> --- cmd/krbctl/cmd/compose.go | 311 ++++++++++++++++++++++++++++++++++++++ cmd/krbctl/cmd/config.go | 237 +++++++++++++++++++++++++++++ cmd/krbctl/cmd/root.go | 22 +++ cmd/krbctl/main.go | 13 ++ go.mod | 3 + go.sum | 9 ++ test/suites/go.sum | 27 +--- 7 files changed, 600 insertions(+), 22 deletions(-) create mode 100644 cmd/krbctl/cmd/compose.go create mode 100644 cmd/krbctl/cmd/config.go create mode 100644 cmd/krbctl/cmd/root.go create mode 100644 cmd/krbctl/main.go diff --git a/cmd/krbctl/cmd/compose.go b/cmd/krbctl/cmd/compose.go new file mode 100644 index 0000000..31a089d --- /dev/null +++ b/cmd/krbctl/cmd/compose.go @@ -0,0 +1,311 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +// composeOptions holds the answers collected from the interactive compose session. +type composeOptions struct { + includeEcho bool + includeObsStack bool + includePostgres bool + includeConnector bool + outputPath string +} + +func newComposeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "compose", + Short: "Interactively generate a base compose.yaml for a Kerberos deployment", + Long: `Walks you through a series of prompts to build a compose.yaml that can +be used to run a Kerberos deployment. Optional sections (observability stack, +postgres, admin-connector, echo) can be included or skipped at each step.`, + RunE: runCompose, + } + + cmd.Flags().StringP("output", "o", "compose.yaml", "Path to write the generated compose.yaml") + + return cmd +} + +func runCompose(cmd *cobra.Command, _ []string) error { + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + scanner := bufio.NewScanner(os.Stdin) + opts := &composeOptions{outputPath: output} + + fmt.Fprintln(os.Stdout, "=== Kerberos compose.yaml generator ===") + fmt.Fprintln(os.Stdout) + + opts.includeEcho = promptYesNo(scanner, + "Include the echo service (useful for testing backends)? [y/N]") + + opts.includeObsStack = promptYesNo(scanner, + "Include the observability stack (Prometheus, Grafana, Jaeger)? [y/N]") + + opts.includePostgres = promptYesNo(scanner, + "Include PostgreSQL as the persistence backend? [y/N]") + + opts.includeConnector = promptYesNo(scanner, + "Include the admin-connector service? [y/N]") + + content := buildCompose(opts) + + if err := os.WriteFile(opts.outputPath, []byte(content), 0o600); err != nil { + return fmt.Errorf("failed to write compose file: %w", err) + } + + fmt.Fprintf(os.Stdout, "\ncompose.yaml written to %s\n", opts.outputPath) + + return nil +} + +// promptYesNo prints the question and reads a y/n answer from the scanner. +// Returns true for "y"/"yes", false otherwise (default: false). +func promptYesNo(scanner *bufio.Scanner, question string) bool { + fmt.Fprint(os.Stdout, question+" ") + + if scanner.Scan() { + switch strings.TrimSpace(strings.ToLower(scanner.Text())) { + case "y", "yes": + return true + default: + return false + } + } + + return false +} + +func buildCompose(opts *composeOptions) string { + var b strings.Builder + + b.WriteString("services:\n") + writePostgresService(&b, opts) + writeKerberosService(&b, opts) + writeEchoService(&b, opts) + writeConnectorService(&b, opts) + writeObsServices(&b, opts) + writeVolumes(&b, opts) + + return b.String() +} + +func writePostgresService(b *strings.Builder, opts *composeOptions) { + if !opts.includePostgres { + return + } + + b.WriteString(` postgres: + image: "postgres:18.4-alpine3.23" + pull_policy: if_not_present + environment: + - POSTGRES_DB=kerberos + - POSTGRES_USER=kerberos + - POSTGRES_PASSWORD=kerberos + restart: on-failure + healthcheck: + test: ["CMD-SHELL", "psql -U kerberos -d kerberos -c 'SELECT 1' -q -t 2>/dev/null | grep -q 1"] + interval: 1s + timeout: 2s + retries: 10 + volumes: + - postgres:/var/lib/postgresql/18/docker + +`) +} + +func writeKerberosService(b *strings.Builder, opts *composeOptions) { + b.WriteString(` kerberos: + image: "ghcr.io/trebent/kerberos:${VERSION:-unset}" + command: --config /config/config.json + pull_policy: if_not_present +`) + + if opts.includePostgres { + b.WriteString(` depends_on: + postgres: + condition: service_healthy +`) + } + + b.WriteString(` restart: on-failure + ports: + - ${KERBEROS_PORT:-30000}:${KERBEROS_PORT:-30000} + - ${KERBEROS_ADMIN_PORT:-30001}:${KERBEROS_ADMIN_PORT:-30001} +`) + + if opts.includeObsStack { + b.WriteString(" - ${KERBEROS_METRICS_PORT:-9464}:${KERBEROS_METRICS_PORT:-9464}\n") + } + + b.WriteString(` environment: + - LOG_TO_CONSOLE=1 + - LOG_VERBOSITY=${LOG_VERBOSITY:-20} + - PORT=${KERBEROS_PORT:-30000} + - ADMIN_PORT=${KERBEROS_ADMIN_PORT:-30001} + - VERSION=${VERSION:-unset} +`) + + writeOtelEnv(b, opts.includeObsStack, "kerberos", "${KERBEROS_METRICS_PORT:-9464}") + + b.WriteString(` volumes: + - ./config:/config:ro + +`) +} + +func writeEchoService(b *strings.Builder, opts *composeOptions) { + if !opts.includeEcho { + return + } + + b.WriteString(` echo: + image: "ghcr.io/trebent/kerberos/echo:${VERSION:-unset}" + pull_policy: if_not_present + restart: on-failure + ports: + - ${ECHO_PORT:-15000}:${ECHO_PORT:-15000} +`) + + if opts.includeObsStack { + b.WriteString(" - ${ECHO_METRICS_PORT:-9463}:${ECHO_METRICS_PORT:-9463}\n") + } + + b.WriteString(` environment: + - PORT=${ECHO_PORT:-15000} +`) + + writeOtelEnv(b, opts.includeObsStack, "echo", "${ECHO_METRICS_PORT:-9463}") + b.WriteString("\n") +} + +func writeConnectorService(b *strings.Builder, opts *composeOptions) { + if !opts.includeConnector { + return + } + + b.WriteString(` connector: + image: "ghcr.io/trebent/kerberos/admin-connector:${VERSION:-unset}" + command: --config /config/connector.json + pull_policy: if_not_present + depends_on: + kerberos: + condition: service_started + restart: on-failure + ports: + - ${CONNECTOR_PORT:-30100}:${CONNECTOR_PORT:-30100} +`) + + if opts.includeObsStack { + b.WriteString(" - ${CONNECTOR_METRICS_PORT:-9462}:${CONNECTOR_METRICS_PORT:-9462}\n") + } + + b.WriteString(` environment: + - LOG_TO_CONSOLE=true + - LOG_VERBOSITY=${LOG_VERBOSITY:-20} + - VERSION=${VERSION:-unset} + - PORT=${CONNECTOR_PORT:-30100} +`) + + writeOtelEnv(b, opts.includeObsStack, "connector", "${CONNECTOR_METRICS_PORT:-9462}") + + b.WriteString(` volumes: + - ./config/connector:/config:ro + +`) +} + +func writeObsServices(b *strings.Builder, opts *composeOptions) { + if !opts.includeObsStack { + return + } + + b.WriteString(` prometheus: + image: "prom/prometheus:v3" + pull_policy: if_not_present + command: ["--config.file=/prometheus.yml", "--storage.tsdb.path", "/prometheus/data", + "--storage.tsdb.retention.size", "1GB"] + restart: on-failure + ports: + - ${PROM_PORT:-9090}:9090 + volumes: + - ./config/prometheus/prometheus.yml:/prometheus.yml + - prometheus:/prometheus + + grafana: + image: "grafana/grafana:13.1" + pull_policy: if_not_present + restart: on-failure + ports: + - ${GRAFANA_PORT:-3000}:3000 + volumes: + - ./config/grafana/grafana-datasources.yml:/etc/grafana/provisioning/datasources/grafana-datasources.yml + - ./config/grafana/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yml + - grafana:/var/lib/grafana + + jaeger-init: + image: busybox:1.38 + pull_policy: if_not_present + command: ["sh", "-c", "chown 10001:0 /jaeger"] + restart: on-failure + volumes: + - jaeger:/jaeger + + jaeger: + image: "jaegertracing/jaeger:2.20.0" + pull_policy: if_not_present + depends_on: + jaeger-init: + condition: service_completed_successfully + restart: on-failure + command: --config /jaeger.yml + ports: + - 16686:16686 + volumes: + - ./config/jaeger/jaeger.yml:/jaeger.yml + - jaeger:/jaeger + +`) +} + +func writeOtelEnv(b *strings.Builder, withObs bool, hostname, metricsPort string) { + if withObs { + fmt.Fprintf(b, " - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317\n") + fmt.Fprintf(b, " - OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=grpc\n") + fmt.Fprintf(b, " - OTEL_METRICS_EXPORTER=prometheus\n") + fmt.Fprintf(b, " - OTEL_EXPORTER_PROMETHEUS_HOST=%s\n", hostname) + fmt.Fprintf(b, " - OTEL_EXPORTER_PROMETHEUS_PORT=%s\n", metricsPort) + } else { + b.WriteString(" - OTEL_METRICS_EXPORTER=none\n") + b.WriteString(" - OTEL_TRACES_EXPORTER=none\n") + } +} + +func writeVolumes(b *strings.Builder, opts *composeOptions) { + var volumes []string + + if opts.includePostgres { + volumes = append(volumes, " postgres:") + } + + if opts.includeObsStack { + volumes = append(volumes, " prometheus:", " grafana:", " jaeger:") + } + + if len(volumes) == 0 { + return + } + + b.WriteString("volumes:\n") + b.WriteString(strings.Join(volumes, "\n")) + b.WriteString("\n") +} diff --git a/cmd/krbctl/cmd/config.go b/cmd/krbctl/cmd/config.go new file mode 100644 index 0000000..5e7cc59 --- /dev/null +++ b/cmd/krbctl/cmd/config.go @@ -0,0 +1,237 @@ +package cmd + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + + "github.com/spf13/cobra" +) + +const ( + driverPostgres = "postgres" + driverSQLite = "sqlite" + defaultKRBDB = "kerberos" +) + +// configOptions holds the answers collected from the interactive config session. +type configOptions struct { + backends []backendEntry + includeAuth bool + includeObs bool + persistenceMode string // "sqlite" or "postgres" + outputPath string +} + +type backendEntry struct { + name string + host string + port int +} + +func newConfigCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "config", + Short: "Interactively generate a base Kerberos configuration file", + Long: `Walks you through a series of prompts to build a base Kerberos JSON +configuration file. Mandatory sections are always included; optional sections +(auth, observability, postgres persistence) can be skipped.`, + RunE: runConfig, + } + + cmd.Flags().StringP("output", "o", "config.json", "Path to write the generated config.json") + + return cmd +} + +func runConfig(cmd *cobra.Command, _ []string) error { + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + scanner := bufio.NewScanner(os.Stdin) + opts := &configOptions{outputPath: output} + + fmt.Fprintln(os.Stdout, "=== Kerberos config.json generator ===") + fmt.Fprintln(os.Stdout) + + // Backend targets (mandatory — gateway needs at least one) + opts.backends = promptBackends(scanner) + + // Optional: auth + opts.includeAuth = promptYesNo(scanner, + "Include the auth section (basic authentication)? [y/N]") + + // Optional: observability + opts.includeObs = promptYesNo(scanner, + "Include the observability section? [y/N]") + + // Optional: postgres persistence + if promptYesNo(scanner, "Use PostgreSQL as the persistence backend (default: SQLite)? [y/N]") { + opts.persistenceMode = driverPostgres + } else { + opts.persistenceMode = driverSQLite + } + + content, err := buildConfig(opts) + if err != nil { + return fmt.Errorf("failed to build config: %w", err) + } + + if err := os.WriteFile(opts.outputPath, content, 0o600); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + fmt.Fprintf(os.Stdout, "\nconfig.json written to %s\n", opts.outputPath) + + return nil +} + +// promptBackends collects one or more backend target entries from the user. +func promptBackends(scanner *bufio.Scanner) []backendEntry { + fmt.Fprintln(os.Stdout, "Configure gateway backend targets.") + fmt.Fprintln( + os.Stdout, + "(At least one backend is required. Press Enter with an empty name to finish.)", + ) + fmt.Fprintln(os.Stdout) + + var backends []backendEntry + + for { + name := promptString( + scanner, + fmt.Sprintf(" Backend %d name (e.g. \"my-api\"): ", len(backends)+1), + ) + if name == "" { + if len(backends) == 0 { + fmt.Fprintln(os.Stdout, " At least one backend is required. Please enter a name.") + continue + } + + break + } + + host := promptString(scanner, " Host (e.g. \"my-api\" or \"localhost\"): ") + if host == "" { + host = "localhost" + } + + port := parsePort(promptString(scanner, " Port (e.g. 8080): ")) + + backends = append(backends, backendEntry{name: name, host: host, port: port}) + fmt.Fprintln(os.Stdout) + + if !promptYesNo(scanner, " Add another backend? [y/N]") { + break + } + + fmt.Fprintln(os.Stdout) + } + + return backends +} + +func parsePort(raw string) int { + const defaultPort = 8080 + + port, err := strconv.Atoi(strings.TrimSpace(raw)) + if err != nil || port < 1 || port > 65535 { + fmt.Fprintln(os.Stdout, " Invalid port, defaulting to 8080.") + + return defaultPort + } + + return port +} + +// promptString prints the question and reads a line from the scanner. +func promptString(scanner *bufio.Scanner, question string) string { + fmt.Fprint(os.Stdout, question) + + if scanner.Scan() { + return strings.TrimSpace(scanner.Text()) + } + + return "" +} + +//nolint:cyclop // config builder requires branching per optional section +func buildConfig(opts *configOptions) ([]byte, error) { + backends := make([]map[string]any, 0, len(opts.backends)) + for _, b := range opts.backends { + backends = append(backends, map[string]any{ + "name": b.name, + "host": b.host, + "port": b.port, + }) + } + + root := map[string]any{ + "gateway": map[string]any{ + "router": map[string]any{ + "backends": backends, + }, + }, + } + + if opts.includeObs { + root["observability"] = map[string]any{ + "enabled": true, + "runtimeMetrics": true, + } + } + + if opts.includeAuth && len(opts.backends) > 0 { + root["auth"] = buildAuthSection(opts.backends) + } + + root["persistence"] = buildPersistenceSection(opts.persistenceMode) + + return json.MarshalIndent(root, "", " ") +} + +func buildAuthSection(backends []backendEntry) map[string]any { + mappings := make([]map[string]any, 0, len(backends)) + for _, b := range backends { + mappings = append(mappings, map[string]any{ + "backend": b.name, + "method": "basic", + "exempt": []string{}, + }) + } + + return map[string]any{ + "methods": map[string]any{ + "basic": map[string]any{}, + }, + "scheme": map[string]any{ + "mappings": mappings, + }, + "order": 1, + } +} + +func buildPersistenceSection(mode string) map[string]any { + if mode == driverPostgres { + return map[string]any{ + "driver": driverPostgres, + "address": "postgres:5432", + "postgres": map[string]any{ + "database": defaultKRBDB, + "username": defaultKRBDB, + "password": defaultKRBDB, + "sslMode": "disable", + }, + } + } + + return map[string]any{ + "driver": driverSQLite, + "address": "krb.db", + } +} diff --git a/cmd/krbctl/cmd/root.go b/cmd/krbctl/cmd/root.go new file mode 100644 index 0000000..0c864b3 --- /dev/null +++ b/cmd/krbctl/cmd/root.go @@ -0,0 +1,22 @@ +// Package cmd contains all krbctl CLI commands. +package cmd + +import ( + "github.com/spf13/cobra" +) + +// Execute adds all child commands to the root command and runs it. +func Execute() error { + root := &cobra.Command{ + Use: "krbctl", + Short: "krbctl is the Kerberos deployment CLI", + Long: `krbctl helps you set up a Kerberos deployment by generating +a base compose.yaml and a base kerberos configuration file through +interactive prompts.`, + } + + root.AddCommand(newComposeCmd()) + root.AddCommand(newConfigCmd()) + + return root.Execute() +} diff --git a/cmd/krbctl/main.go b/cmd/krbctl/main.go new file mode 100644 index 0000000..e4fe07b --- /dev/null +++ b/cmd/krbctl/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "os" + + "github.com/trebent/kerberos/cmd/krbctl/cmd" +) + +func main() { + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index ea2f700..920de5c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/lib/pq v1.12.3 github.com/oapi-codegen/nethttp-middleware v1.2.0 github.com/oapi-codegen/runtime v1.6.0 + github.com/spf13/cobra v1.9.1 github.com/trebent/envparser v1.0.8 github.com/trebent/zerologr v1.1.1 github.com/xeipuuv/gojsonschema v1.2.0 @@ -35,6 +36,7 @@ require ( github.com/go-openapi/jsonpointer v1.0.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -50,6 +52,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rs/zerolog v1.35.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/go.sum b/go.sum index db79efc..1c474a8 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,7 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -40,6 +41,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF2 github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= @@ -88,8 +91,13 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -186,6 +194,7 @@ google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/test/suites/go.sum b/test/suites/go.sum index 693d205..bd35a10 100644 --- a/test/suites/go.sum +++ b/test/suites/go.sum @@ -16,8 +16,6 @@ github.com/dprotaso/go-yit v0.0.0-20260623150633-6f1ed93922d1/go.mod h1:EjiB/8UO github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/getkin/kin-openapi v0.145.0 h1:htBX+Q7SevVaCUqymFegUKzH2WCbewl9tsmyn2FMGWY= -github.com/getkin/kin-openapi v0.145.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo= github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -82,16 +80,12 @@ github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/speakeasy-api/jsonpath v0.6.3 h1:c+QPwzAOdrWvzycuc9HFsIZcxKIaWcNpC+xhOW9rJxU= github.com/speakeasy-api/jsonpath v0.6.3/go.mod h1:2cXloNuQ+RSXi5HTRaeBh7JEmjRXTiaKpFTdZiL7URI= -github.com/speakeasy-api/openapi v1.24.0 h1:opoD27rupX7zBVPq1HkIGLeMOzNNA7JalhYP8q34i04= -github.com/speakeasy-api/openapi v1.24.0/go.mod h1:g3+dIMe0AYgbbGvnlQZqesmjAVWSm9BmsjLevnefQrg= github.com/speakeasy-api/openapi v1.24.1 h1:e8rkoiq1q8vJ/Ru1pjdlABlgkbG581M4XRadhaHtT3c= github.com/speakeasy-api/openapi v1.24.1/go.mod h1:9gGkzi9jNspEbcB08zta+IjzAi5Zy4QgSEQfF/LSrbQ= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= @@ -107,27 +101,20 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= @@ -174,12 +161,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea h1:kVhQEPTpKQahD5+JSBTfBB19wcgQTTjAIn45MBqnyHk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 57eeda94011d0fe9f9900504779a05ac3cdb968b Mon Sep 17 00:00:00 2001 From: maansaake Date: Mon, 17 Aug 2026 21:02:01 +0200 Subject: [PATCH 02/13] update file paths --- cmd/krbctl/cmd/compose.go | 16 ++++++++-------- cmd/krbctl/cmd/config.go | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/krbctl/cmd/compose.go b/cmd/krbctl/cmd/compose.go index 31a089d..0d08ca8 100644 --- a/cmd/krbctl/cmd/compose.go +++ b/cmd/krbctl/cmd/compose.go @@ -126,7 +126,7 @@ func writePostgresService(b *strings.Builder, opts *composeOptions) { func writeKerberosService(b *strings.Builder, opts *composeOptions) { b.WriteString(` kerberos: image: "ghcr.io/trebent/kerberos:${VERSION:-unset}" - command: --config /config/config.json + command: --config /krb.json pull_policy: if_not_present `) @@ -158,7 +158,7 @@ func writeKerberosService(b *strings.Builder, opts *composeOptions) { writeOtelEnv(b, opts.includeObsStack, "kerberos", "${KERBEROS_METRICS_PORT:-9464}") b.WriteString(` volumes: - - ./config:/config:ro + - ./krb.json:/krb.json:ro `) } @@ -195,7 +195,7 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { b.WriteString(` connector: image: "ghcr.io/trebent/kerberos/admin-connector:${VERSION:-unset}" - command: --config /config/connector.json + command: --config /connector.json pull_policy: if_not_present depends_on: kerberos: @@ -219,7 +219,7 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { writeOtelEnv(b, opts.includeObsStack, "connector", "${CONNECTOR_METRICS_PORT:-9462}") b.WriteString(` volumes: - - ./config/connector:/config:ro + - ./connector.json:/connector.json:ro `) } @@ -238,7 +238,7 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { ports: - ${PROM_PORT:-9090}:9090 volumes: - - ./config/prometheus/prometheus.yml:/prometheus.yml + - ./prometheus.yml:/prometheus.yml - prometheus:/prometheus grafana: @@ -248,8 +248,8 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { ports: - ${GRAFANA_PORT:-3000}:3000 volumes: - - ./config/grafana/grafana-datasources.yml:/etc/grafana/provisioning/datasources/grafana-datasources.yml - - ./config/grafana/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yml + - ./grafana/grafana-datasources.yml:/etc/grafana/provisioning/datasources/grafana-datasources.yml + - ./grafana/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yml - grafana:/var/lib/grafana jaeger-init: @@ -271,7 +271,7 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { ports: - 16686:16686 volumes: - - ./config/jaeger/jaeger.yml:/jaeger.yml + - ./jaeger.yml:/jaeger.yml - jaeger:/jaeger `) diff --git a/cmd/krbctl/cmd/config.go b/cmd/krbctl/cmd/config.go index 5e7cc59..ab975ec 100644 --- a/cmd/krbctl/cmd/config.go +++ b/cmd/krbctl/cmd/config.go @@ -42,7 +42,7 @@ configuration file. Mandatory sections are always included; optional sections RunE: runConfig, } - cmd.Flags().StringP("output", "o", "config.json", "Path to write the generated config.json") + cmd.Flags().StringP("output", "o", "krb.json", "Path to write the generated krb.json") return cmd } @@ -56,7 +56,7 @@ func runConfig(cmd *cobra.Command, _ []string) error { scanner := bufio.NewScanner(os.Stdin) opts := &configOptions{outputPath: output} - fmt.Fprintln(os.Stdout, "=== Kerberos config.json generator ===") + fmt.Fprintln(os.Stdout, "=== Kerberos krb.json generator ===") fmt.Fprintln(os.Stdout) // Backend targets (mandatory — gateway needs at least one) @@ -86,7 +86,7 @@ func runConfig(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to write config file: %w", err) } - fmt.Fprintf(os.Stdout, "\nconfig.json written to %s\n", opts.outputPath) + fmt.Fprintf(os.Stdout, "\nkrb.json written to %s\n", opts.outputPath) return nil } From a9ee40ec34b2f60da9c3d3ab34e868bd2c0da57a Mon Sep 17 00:00:00 2001 From: maansaake Date: Mon, 17 Aug 2026 21:37:53 +0200 Subject: [PATCH 03/13] feat(krbctl): TUI overhaul with huh forms and obs/connector config generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace raw bufio.Scanner stdin prompts in krbctl with charmbracelet/huh interactive forms, providing a polished, navigable TUI experience. Changes: - Add charmbracelet/huh dependency - compose: rewrite all prompts as huh Confirm form groups - compose: update grafana service mounts to include grafana.ini and dashboard JSON files alongside existing provisioning YAMLs - config: rewrite all prompts (Input, Select, Confirm, MultiSelect) - config: add observability config section (after kerberos section): - MultiSelect for Prometheus scrape targets (kerberos/echo/connector/jaeger) - Select for Grafana database backend (postgres/sqlite) - Confirm for Grafana anonymous access - Generates prometheus.yml, grafana/grafana.ini (slim — [database] + [auth.anonymous] only), grafana/grafana-datasources.yml, grafana/grafana-dashboards.yml, grafana dashboard JSONs, jaeger.yml - config: add admin-connector section generating connector.json - Embed Grafana dashboard JSON files from test/config/grafana/ at build time - Add unit tests for all new builder functions (12 tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cmd/assets/grafana/kerberos_http.json | 2109 +++++++++++++++++ .../cmd/assets/grafana/kerberos_runtime.json | 730 ++++++ cmd/krbctl/cmd/assets/grafana/prometheus.json | 573 +++++ cmd/krbctl/cmd/builders_test.go | 301 +++ cmd/krbctl/cmd/compose.go | 64 +- cmd/krbctl/cmd/config.go | 323 ++- cmd/krbctl/cmd/connectorconfig.go | 28 + cmd/krbctl/cmd/obsconfig.go | 220 ++ go.mod | 23 + go.sum | 46 + 10 files changed, 4323 insertions(+), 94 deletions(-) create mode 100644 cmd/krbctl/cmd/assets/grafana/kerberos_http.json create mode 100644 cmd/krbctl/cmd/assets/grafana/kerberos_runtime.json create mode 100644 cmd/krbctl/cmd/assets/grafana/prometheus.json create mode 100644 cmd/krbctl/cmd/builders_test.go create mode 100644 cmd/krbctl/cmd/connectorconfig.go create mode 100644 cmd/krbctl/cmd/obsconfig.go diff --git a/cmd/krbctl/cmd/assets/grafana/kerberos_http.json b/cmd/krbctl/cmd/assets/grafana/kerberos_http.json new file mode 100644 index 0000000..aad2652 --- /dev/null +++ b/cmd/krbctl/cmd/assets/grafana/kerberos_http.json @@ -0,0 +1,2109 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 27, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(request_count_total[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "{{krb_backend}}: {{http_method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Rate of requests", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_count_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: {{http_method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "response_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: {{http_method}} {{http_status_code}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response total", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 20, + "panels": [], + "title": "Responses", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 0, + "y": 23 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "response_size_bytes_bucket{http_method=\"GET\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response size (bytes) bucket: GET", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 8, + "y": 23 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "response_size_bytes_bucket{http_method=\"POST\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response size (bytes) bucket: POST", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 23 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "response_size_bytes_bucket{http_method=\"PUT\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response size (bytes) bucket: PUT", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 0, + "y": 34 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "response_size_bytes_bucket{http_method=\"PATCH\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response size (bytes) bucket: PATCH", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 8, + "y": 34 + }, + "id": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "response_size_bytes_bucket{http_method=\"DELETE\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response size (bytes) bucket: DELETE", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 34 + }, + "id": 26, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "response_size_bytes_bucket{http_method=\"OPTIONS\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response size (bytes) bucket: OPTIONS", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 45 + }, + "id": 4, + "panels": [], + "title": "Requests", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 0, + "y": 46 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_duration_milliseconds_bucket{http_method=\"GET\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request duration (ms) bucket: GET", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 8, + "y": 46 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_duration_milliseconds_bucket{http_method=\"POST\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request duration (ms) bucket: POST", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 46 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_duration_milliseconds_bucket{http_method=\"PUT\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request duration (ms) bucket: PUT", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 0, + "y": 57 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_duration_milliseconds_bucket{http_method=\"PATCH\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request duration (ms) bucket: PATCH", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 8, + "y": 57 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_duration_milliseconds_bucket{http_method=\"DELETE\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request duration (ms) bucket: DELETE", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 57 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_duration_milliseconds_bucket{http_method=\"OPTIONS\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request duration (ms) bucket: OPTIONS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 0, + "y": 68 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_size_bytes_bucket{http_method=\"GET\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request size (bytes) bucket: GET", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 8, + "y": 68 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_size_bytes_bucket{http_method=\"POST\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request size (bytes) bucket: POST", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 68 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_size_bytes_bucket{http_method=\"PUT\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request size (bytes) bucket: PUT", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 0, + "y": 79 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_size_bytes_bucket{http_method=\"PATCH\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request size (bytes) bucket: PATCH", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 8, + "y": 79 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_size_bytes_bucket{http_method=\"DELETE\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request size (bytes) bucket: DELETE", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 79 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Name", + "sortDesc": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "request_size_bytes_bucket{http_method=\"OPTIONS\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{krb_backend}}: le {{le}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request size (bytes) bucket: OPTIONS", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Kerberos HTTP", + "uid": "9179fb85-6e9e-42e5-8527-230ec6d6c0b2", + "version": 1 +} \ No newline at end of file diff --git a/cmd/krbctl/cmd/assets/grafana/kerberos_runtime.json b/cmd/krbctl/cmd/assets/grafana/kerberos_runtime.json new file mode 100644 index 0000000..d1c8311 --- /dev/null +++ b/cmd/krbctl/cmd/assets/grafana/kerberos_runtime.json @@ -0,0 +1,730 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Goroutines", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "go_goroutine_count", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Goroutines", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 21, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "go_processor_limit", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Processor limit", + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 17, + "panels": [], + "title": "Memory", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 11 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "go_memory_allocated_bytes_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Heap: allocated bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 11 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "go_memory_allocations_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Heap: allocation total", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "go_memory_used_bytes", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "Used {{job}}: {{go_memory_type}}", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "go_memory_limit_bytes", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Limit {{job}}", + "range": true, + "refId": "B", + "useBackend": false + } + ], + "title": "Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "go_memory_gc_goal_bytes", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "GC goal", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 19, + "panels": [], + "title": "Observability", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "promhttp_metric_handler_errors_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{job}}: cause = {{cause}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "promhttp gathering errors", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "auto", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Kerberos runtime", + "uid": "ec321aef-6a36-42aa-a972-08d3d17660be", + "version": 1 +} \ No newline at end of file diff --git a/cmd/krbctl/cmd/assets/grafana/prometheus.json b/cmd/krbctl/cmd/assets/grafana/prometheus.json new file mode 100644 index 0000000..3b8d794 --- /dev/null +++ b/cmd/krbctl/cmd/assets/grafana/prometheus.json @@ -0,0 +1,573 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "cellOptions": { + "type": "auto", + "wrapText": false + }, + "inspect": false + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "target_info", + "format": "table", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Target information", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "instance", + "job", + "service.name", + "telemetry.sdk.language", + "telemetry.sdk.name", + "telemetry.sdk.version" + ], + "mode": "columns" + } + }, + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "instance", + "job", + "service.name", + "Value", + "service.version" + ] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": { + "Value": 4, + "instance": 3, + "job": 2, + "service.name": 0, + "service.version": 1 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 5 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "scrape_series_added", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Scrape series added", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "Seconds", + "axisPlacement": "left", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 5 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "scrape_duration_seconds", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Scrape duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "scrape_samples_scraped", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Samples scraped", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "scrape_samples_post_metric_relabeling", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Scrape samples post metric relableing", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Prometheus status", + "uid": "6d3ea7ab-5878-46f3-9b5f-2a021b205cc5", + "version": 1 +} \ No newline at end of file diff --git a/cmd/krbctl/cmd/builders_test.go b/cmd/krbctl/cmd/builders_test.go new file mode 100644 index 0000000..0830191 --- /dev/null +++ b/cmd/krbctl/cmd/builders_test.go @@ -0,0 +1,301 @@ +package cmd + +import ( + "encoding/json" + "strings" + "testing" +) + +// ---- buildConfig ---- + +func TestBuildConfig_BasicBackend(t *testing.T) { + t.Parallel() + + opts := &configOptions{ + backends: []backendEntry{{name: "api", host: "localhost", port: 8080}}, + persistenceMode: driverSQLite, + } + + data, err := buildConfig(opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var result map[string]any + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + gw, ok := result["gateway"].(map[string]any) + if !ok { + t.Fatal("missing gateway section") + } + + router, ok := gw["router"].(map[string]any) + if !ok { + t.Fatal("missing router section") + } + + backends, ok := router["backends"].([]any) + if !ok || len(backends) != 1 { + t.Fatalf("expected 1 backend, got %v", router["backends"]) + } +} + +func TestBuildConfig_IncludesObs(t *testing.T) { + t.Parallel() + + opts := &configOptions{ + backends: []backendEntry{{name: "api", host: "api", port: 9000}}, + includeObs: true, + persistenceMode: driverSQLite, + } + + data, _ := buildConfig(opts) + + var result map[string]any + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + if _, ok := result["observability"]; !ok { + t.Error("expected observability section") + } +} + +func TestBuildConfig_PostgresPersistence(t *testing.T) { + t.Parallel() + + opts := &configOptions{ + backends: []backendEntry{{name: "svc", host: "svc", port: 80}}, + persistenceMode: driverPostgres, + } + + data, _ := buildConfig(opts) + + var result map[string]any + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + persistence, ok := result["persistence"].(map[string]any) + if !ok { + t.Fatal("missing persistence section") + } + + if persistence["driver"] != driverPostgres { + t.Errorf("expected driver=postgres, got %v", persistence["driver"]) + } +} + +// ---- buildPrometheusYML ---- + +func TestBuildPrometheusYML_AllTargets(t *testing.T) { + t.Parallel() + + opts := &obsConfigOptions{ + scrapeTargets: []string{"kerberos", "echo", "connector", "jaeger"}, + } + + yml := buildPrometheusYML(opts) + + for _, target := range opts.scrapeTargets { + if !strings.Contains(yml, target) { + t.Errorf("expected target %q in prometheus.yml", target) + } + } + + if !strings.Contains(yml, "kerberos:9464") { + t.Error("expected kerberos:9464") + } + + if !strings.Contains(yml, "echo:9463") { + t.Error("expected echo:9463") + } + + if !strings.Contains(yml, "connector:9462") { + t.Error("expected connector:9462") + } + + if !strings.Contains(yml, "jaeger:8888") { + t.Error("expected jaeger:8888") + } +} + +func TestBuildPrometheusYML_SelectedTargets(t *testing.T) { + t.Parallel() + + opts := &obsConfigOptions{ + scrapeTargets: []string{"kerberos"}, + } + + yml := buildPrometheusYML(opts) + + if !strings.Contains(yml, "kerberos:9464") { + t.Error("expected kerberos:9464") + } + + if strings.Contains(yml, "echo") { + t.Error("echo should not be in output") + } +} + +// ---- buildGrafanaINI ---- + +func TestBuildGrafanaINI_Postgres(t *testing.T) { + t.Parallel() + + opts := &obsConfigOptions{ + grafanaDB: driverPostgres, + grafanaAnonymous: true, + } + + ini := buildGrafanaINI(opts) + + if !strings.Contains(ini, "type = postgres") { + t.Error("expected type = postgres") + } + + if !strings.Contains(ini, "host = postgres:5432") { + t.Error("expected host = postgres:5432") + } + + if !strings.Contains(ini, "enabled = true") { + t.Error("expected enabled = true in auth.anonymous") + } +} + +func TestBuildGrafanaINI_SQLite_NoAnon(t *testing.T) { + t.Parallel() + + opts := &obsConfigOptions{ + grafanaDB: "sqlite3", + grafanaAnonymous: false, + } + + ini := buildGrafanaINI(opts) + + if !strings.Contains(ini, "type = sqlite3") { + t.Error("expected type = sqlite3") + } + + if strings.Contains(ini, "host =") { + t.Error("sqlite config should not contain host") + } + + if !strings.Contains(ini, "enabled = false") { + t.Error("expected enabled = false in auth.anonymous") + } +} + +// ---- buildGrafanaDatasourcesYML ---- + +func TestBuildGrafanaDatasourcesYML(t *testing.T) { + t.Parallel() + + yml := buildGrafanaDatasourcesYML() + + if !strings.Contains(yml, "prometheus") { + t.Error("expected prometheus datasource") + } + + if !strings.Contains(yml, "http://prometheus:9090") { + t.Error("expected prometheus URL") + } +} + +// ---- buildGrafanaDashboardsYML ---- + +func TestBuildGrafanaDashboardsYML(t *testing.T) { + t.Parallel() + + yml := buildGrafanaDashboardsYML() + + for _, dashboard := range []string{"prometheus.json", "kerberos_runtime.json", "kerberos_http.json"} { + if !strings.Contains(yml, dashboard) { + t.Errorf("expected dashboard %q in grafana-dashboards.yml", dashboard) + } + } +} + +// ---- buildJaegerYML ---- + +func TestBuildJaegerYML(t *testing.T) { + t.Parallel() + + yml := buildJaegerYML() + + if !strings.Contains(yml, "otlp") { + t.Error("expected otlp receiver in jaeger.yml") + } + + if !strings.Contains(yml, "badger_store") { + t.Error("expected badger_store backend") + } + + if !strings.Contains(yml, "16686") { + t.Error("expected jaeger query port 16686") + } +} + +// ---- buildConnectorJSON ---- + +func TestBuildConnectorJSON_SQLite(t *testing.T) { + t.Parallel() + + opts := &connectorOptions{ + corsOrigin: "http://localhost:3000", + persistenceMode: driverSQLite, + } + + data, err := buildConnectorJSON(opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var result map[string]any + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + origins, ok := result["origins"].(map[string]any) + if !ok { + t.Fatal("missing origins section") + } + + allowed, ok := origins["allowedOrigins"].([]any) + if !ok || len(allowed) == 0 { + t.Fatal("expected at least one allowed origin") + } + + if allowed[0] != "http://localhost:3000" { + t.Errorf("expected origin http://localhost:3000, got %v", allowed[0]) + } + + persistence, ok := result["persistence"].(map[string]any) + if !ok { + t.Fatal("missing persistence section") + } + + if persistence["driver"] != driverSQLite { + t.Errorf("expected driver=sqlite, got %v", persistence["driver"]) + } +} + +func TestBuildConnectorJSON_DefaultOrigin(t *testing.T) { + t.Parallel() + + opts := &connectorOptions{ + corsOrigin: "", + persistenceMode: driverSQLite, + } + + data, err := buildConnectorJSON(opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !strings.Contains(string(data), "http://kerberos:30001") { + t.Error("expected default origin http://kerberos:30001") + } +} diff --git a/cmd/krbctl/cmd/compose.go b/cmd/krbctl/cmd/compose.go index 0d08ca8..d533b88 100644 --- a/cmd/krbctl/cmd/compose.go +++ b/cmd/krbctl/cmd/compose.go @@ -1,11 +1,11 @@ package cmd import ( - "bufio" "fmt" "os" "strings" + "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) @@ -39,23 +39,34 @@ func runCompose(cmd *cobra.Command, _ []string) error { return err } - scanner := bufio.NewScanner(os.Stdin) opts := &composeOptions{outputPath: output} - fmt.Fprintln(os.Stdout, "=== Kerberos compose.yaml generator ===") - fmt.Fprintln(os.Stdout) - - opts.includeEcho = promptYesNo(scanner, - "Include the echo service (useful for testing backends)? [y/N]") - - opts.includeObsStack = promptYesNo(scanner, - "Include the observability stack (Prometheus, Grafana, Jaeger)? [y/N]") - - opts.includePostgres = promptYesNo(scanner, - "Include PostgreSQL as the persistence backend? [y/N]") - - opts.includeConnector = promptYesNo(scanner, - "Include the admin-connector service? [y/N]") + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Include the echo service?"). + Description("Useful for testing backends."). + Value(&opts.includeEcho), + + huh.NewConfirm(). + Title("Include the observability stack?"). + Description("Adds Prometheus, Grafana, and Jaeger services."). + Value(&opts.includeObsStack), + + huh.NewConfirm(). + Title("Include PostgreSQL as the persistence backend?"). + Description("Uses SQLite by default if skipped."). + Value(&opts.includePostgres), + + huh.NewConfirm(). + Title("Include the admin-connector service?"). + Value(&opts.includeConnector), + ), + ) + + if err := form.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) + } content := buildCompose(opts) @@ -68,23 +79,6 @@ func runCompose(cmd *cobra.Command, _ []string) error { return nil } -// promptYesNo prints the question and reads a y/n answer from the scanner. -// Returns true for "y"/"yes", false otherwise (default: false). -func promptYesNo(scanner *bufio.Scanner, question string) bool { - fmt.Fprint(os.Stdout, question+" ") - - if scanner.Scan() { - switch strings.TrimSpace(strings.ToLower(scanner.Text())) { - case "y", "yes": - return true - default: - return false - } - } - - return false -} - func buildCompose(opts *composeOptions) string { var b strings.Builder @@ -248,8 +242,12 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { ports: - ${GRAFANA_PORT:-3000}:3000 volumes: + - ./grafana/grafana.ini:/etc/grafana/grafana.ini - ./grafana/grafana-datasources.yml:/etc/grafana/provisioning/datasources/grafana-datasources.yml - ./grafana/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yml + - ./grafana/prometheus.json:/var/lib/grafana/prometheus.json + - ./grafana/kerberos_runtime.json:/var/lib/grafana/kerberos_runtime.json + - ./grafana/kerberos_http.json:/var/lib/grafana/kerberos_http.json - grafana:/var/lib/grafana jaeger-init: diff --git a/cmd/krbctl/cmd/config.go b/cmd/krbctl/cmd/config.go index ab975ec..4ac61c1 100644 --- a/cmd/krbctl/cmd/config.go +++ b/cmd/krbctl/cmd/config.go @@ -1,29 +1,38 @@ package cmd import ( - "bufio" "encoding/json" "fmt" "os" "strconv" "strings" + "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) const ( - driverPostgres = "postgres" - driverSQLite = "sqlite" - defaultKRBDB = "kerberos" + driverPostgres = "postgres" + driverSQLite = "sqlite" + defaultKRBDB = "kerberos" + defaultConnectorTarget = "http://kerberos:30001" ) -// configOptions holds the answers collected from the interactive config session. +// configOptions holds all answers collected from the interactive config session. type configOptions struct { + // Kerberos gateway backends []backendEntry includeAuth bool - includeObs bool persistenceMode string // "sqlite" or "postgres" outputPath string + + // Observability + includeObs bool + obsOpts obsConfigOptions + + // Admin-connector + includeConnector bool + connectorOpts connectorOptions } type backendEntry struct { @@ -32,13 +41,27 @@ type backendEntry struct { port int } +// obsConfigOptions holds the answers for the observability config section. +type obsConfigOptions struct { + scrapeTargets []string // e.g. ["kerberos","echo","connector","jaeger"] + grafanaDB string // "postgres" or "sqlite" + grafanaAnonymous bool +} + +// connectorOptions holds the answers for the admin-connector config section. +type connectorOptions struct { + targetURL string + corsOrigin string + persistenceMode string // "sqlite" or "postgres" +} + func newConfigCmd() *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Interactively generate a base Kerberos configuration file", Long: `Walks you through a series of prompts to build a base Kerberos JSON configuration file. Mandatory sections are always included; optional sections -(auth, observability, postgres persistence) can be skipped.`, +(auth, observability, postgres persistence, admin-connector) can be skipped.`, RunE: runConfig, } @@ -53,30 +76,36 @@ func runConfig(cmd *cobra.Command, _ []string) error { return err } - scanner := bufio.NewScanner(os.Stdin) - opts := &configOptions{outputPath: output} - - fmt.Fprintln(os.Stdout, "=== Kerberos krb.json generator ===") - fmt.Fprintln(os.Stdout) - - // Backend targets (mandatory — gateway needs at least one) - opts.backends = promptBackends(scanner) + opts := &configOptions{ + outputPath: output, + connectorOpts: connectorOptions{ + targetURL: defaultConnectorTarget, + corsOrigin: defaultConnectorTarget, + persistenceMode: driverSQLite, + }, + obsOpts: obsConfigOptions{ + grafanaDB: driverPostgres, + grafanaAnonymous: true, + }, + } - // Optional: auth - opts.includeAuth = promptYesNo(scanner, - "Include the auth section (basic authentication)? [y/N]") + if err := promptKerberosSection(opts); err != nil { + return err + } - // Optional: observability - opts.includeObs = promptYesNo(scanner, - "Include the observability section? [y/N]") + if opts.includeObs { + if err := promptObsSection(opts); err != nil { + return err + } + } - // Optional: postgres persistence - if promptYesNo(scanner, "Use PostgreSQL as the persistence backend (default: SQLite)? [y/N]") { - opts.persistenceMode = driverPostgres - } else { - opts.persistenceMode = driverSQLite + if opts.includeConnector { + if err := promptConnectorSection(opts); err != nil { + return err + } } + // Write krb.json content, err := buildConfig(opts) if err != nil { return fmt.Errorf("failed to build config: %w", err) @@ -86,78 +115,250 @@ func runConfig(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to write config file: %w", err) } - fmt.Fprintf(os.Stdout, "\nkrb.json written to %s\n", opts.outputPath) + fmt.Fprintf(os.Stdout, "krb.json written to %s\n", opts.outputPath) + + // Write observability config files + if opts.includeObs { + if err := writeObsFiles(opts); err != nil { + return err + } + } + + // Write connector.json + if opts.includeConnector { + connContent, err := buildConnectorJSON(&opts.connectorOpts) + if err != nil { + return fmt.Errorf("failed to build connector config: %w", err) + } + + if err := os.WriteFile("connector.json", connContent, 0o600); err != nil { + return fmt.Errorf("failed to write connector.json: %w", err) + } + + fmt.Fprintln(os.Stdout, "connector.json written to connector.json") + } return nil } -// promptBackends collects one or more backend target entries from the user. -func promptBackends(scanner *bufio.Scanner) []backendEntry { - fmt.Fprintln(os.Stdout, "Configure gateway backend targets.") - fmt.Fprintln( - os.Stdout, - "(At least one backend is required. Press Enter with an empty name to finish.)", +// promptKerberosSection runs the main Kerberos gateway configuration prompts. +func promptKerberosSection(opts *configOptions) error { + if err := promptBackendsHuh(opts); err != nil { + return err + } + + persistenceOpts := []huh.Option[string]{ + huh.NewOption("SQLite (default, file-based)", driverSQLite), + huh.NewOption("PostgreSQL", driverPostgres), + } + opts.persistenceMode = driverSQLite + + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Include the auth section?"). + Description("Enables basic authentication for backend routes."). + Value(&opts.includeAuth), + + huh.NewConfirm(). + Title("Include the observability section?"). + Description("Enables metrics and tracing for Kerberos."). + Value(&opts.includeObs), + + huh.NewSelect[string](). + Title("Persistence backend"). + Options(persistenceOpts...). + Value(&opts.persistenceMode), + + huh.NewConfirm(). + Title("Include the admin-connector?"). + Description("Generates connector.json for the admin-connector service."). + Value(&opts.includeConnector), + ), ) - fmt.Fprintln(os.Stdout) - var backends []backendEntry + if err := form.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) + } + return nil +} + +// promptBackendsHuh collects one or more backend target entries using huh. +func promptBackendsHuh(opts *configOptions) error { for { - name := promptString( - scanner, - fmt.Sprintf(" Backend %d name (e.g. \"my-api\"): ", len(backends)+1), + var ( + name string + host string + portStr string + ) + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title(fmt.Sprintf("Backend %d — name", len(opts.backends)+1)). + Description(`Press Enter with an empty name to finish (at least one required).`). + Placeholder("my-api"). + Value(&name), + ), ) + + if err := form.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) + } + + name = strings.TrimSpace(name) if name == "" { - if len(backends) == 0 { - fmt.Fprintln(os.Stdout, " At least one backend is required. Please enter a name.") + if len(opts.backends) == 0 { + fmt.Fprintln(os.Stderr, "At least one backend is required.") continue } break } - host := promptString(scanner, " Host (e.g. \"my-api\" or \"localhost\"): ") - if host == "" { + detailForm := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title(fmt.Sprintf("Backend %q — host", name)). + Placeholder("localhost"). + Value(&host), + + huh.NewInput(). + Title(fmt.Sprintf("Backend %q — port", name)). + Placeholder("8080"). + Value(&portStr), + ), + ) + + if err := detailForm.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) + } + + if strings.TrimSpace(host) == "" { host = "localhost" } - port := parsePort(promptString(scanner, " Port (e.g. 8080): ")) + port := parsePort(portStr) + opts.backends = append(opts.backends, backendEntry{name: name, host: host, port: port}) - backends = append(backends, backendEntry{name: name, host: host, port: port}) - fmt.Fprintln(os.Stdout) + var addAnother bool + confirmForm := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Add another backend?"). + Value(&addAnother), + ), + ) - if !promptYesNo(scanner, " Add another backend? [y/N]") { + if err := confirmForm.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) + } + + if !addAnother { break } + } + + return nil +} + +// promptObsSection runs the observability configuration prompts. +func promptObsSection(opts *configOptions) error { + opts.obsOpts.scrapeTargets = []string{defaultKRBDB} + + grafanaDBOpts := []huh.Option[string]{ + huh.NewOption("PostgreSQL", driverPostgres), + huh.NewOption("SQLite (Grafana default)", "sqlite3"), + } + + scrapeOpts := []huh.Option[string]{ + huh.NewOption("kerberos (port 9464)", "kerberos"), + huh.NewOption("echo (port 9463)", "echo"), + huh.NewOption("connector (port 9462)", "connector"), + huh.NewOption("jaeger (port 8888)", "jaeger"), + } + + form := huh.NewForm( + huh.NewGroup( + huh.NewMultiSelect[string](). + Title("Prometheus scrape targets"). + Description("Select services that should expose metrics to Prometheus."). + Options(scrapeOpts...). + Value(&opts.obsOpts.scrapeTargets), + + huh.NewSelect[string](). + Title("Grafana database backend"). + Options(grafanaDBOpts...). + Value(&opts.obsOpts.grafanaDB), + + huh.NewConfirm(). + Title("Enable Grafana anonymous access?"). + Description("Allows viewing dashboards without logging in."). + Value(&opts.obsOpts.grafanaAnonymous), + ), + ) - fmt.Fprintln(os.Stdout) + if err := form.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) } - return backends + return nil } -func parsePort(raw string) int { - const defaultPort = 8080 +// promptConnectorSection runs the admin-connector configuration prompts. +func promptConnectorSection(opts *configOptions) error { + persistenceOpts := []huh.Option[string]{ + huh.NewOption("SQLite (default, file-based)", driverSQLite), + huh.NewOption("PostgreSQL", driverPostgres), + } + opts.connectorOpts.persistenceMode = driverSQLite + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Kerberos admin target URL"). + Description("The URL at which the admin-connector can reach Kerberos."). + Placeholder(defaultConnectorTarget). + Value(&opts.connectorOpts.targetURL), + + huh.NewInput(). + Title("Allowed CORS origin"). + Description("The origin browsers are served from (used to allow cross-origin requests)."). + Placeholder(defaultConnectorTarget). + Value(&opts.connectorOpts.corsOrigin), + + huh.NewSelect[string](). + Title("Connector persistence backend"). + Options(persistenceOpts...). + Value(&opts.connectorOpts.persistenceMode), + ), + ) - port, err := strconv.Atoi(strings.TrimSpace(raw)) - if err != nil || port < 1 || port > 65535 { - fmt.Fprintln(os.Stdout, " Invalid port, defaulting to 8080.") + if err := form.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) + } - return defaultPort + if strings.TrimSpace(opts.connectorOpts.targetURL) == "" { + opts.connectorOpts.targetURL = defaultConnectorTarget } - return port + if strings.TrimSpace(opts.connectorOpts.corsOrigin) == "" { + opts.connectorOpts.corsOrigin = defaultConnectorTarget + } + + return nil } -// promptString prints the question and reads a line from the scanner. -func promptString(scanner *bufio.Scanner, question string) string { - fmt.Fprint(os.Stdout, question) +func parsePort(raw string) int { + const defaultPort = 8080 - if scanner.Scan() { - return strings.TrimSpace(scanner.Text()) + port, err := strconv.Atoi(strings.TrimSpace(raw)) + if err != nil || port < 1 || port > 65535 { + return defaultPort } - return "" + return port } //nolint:cyclop // config builder requires branching per optional section diff --git a/cmd/krbctl/cmd/connectorconfig.go b/cmd/krbctl/cmd/connectorconfig.go new file mode 100644 index 0000000..14af24c --- /dev/null +++ b/cmd/krbctl/cmd/connectorconfig.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "encoding/json" + "fmt" +) + +// buildConnectorJSON generates a minimal connector.json. +func buildConnectorJSON(opts *connectorOptions) ([]byte, error) { + origin := opts.corsOrigin + if origin == "" { + origin = defaultConnectorTarget + } + + root := map[string]any{ + "origins": map[string]any{ + "allowedOrigins": []string{origin}, + }, + "persistence": buildPersistenceSection(opts.persistenceMode), + } + + content, err := json.MarshalIndent(root, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal connector config: %w", err) + } + + return content, nil +} diff --git a/cmd/krbctl/cmd/obsconfig.go b/cmd/krbctl/cmd/obsconfig.go new file mode 100644 index 0000000..e57a38a --- /dev/null +++ b/cmd/krbctl/cmd/obsconfig.go @@ -0,0 +1,220 @@ +package cmd + +import ( + _ "embed" + "fmt" + "os" + "strings" +) + +//go:embed assets/grafana/prometheus.json +var grafanaDashboardPrometheus []byte + +//go:embed assets/grafana/kerberos_runtime.json +var grafanaDashboardRuntime []byte + +//go:embed assets/grafana/kerberos_http.json +var grafanaDashboardHTTP []byte + +// scrapeTargetPorts maps each known scrape target to its default host:port. +var scrapeTargetPorts = map[string]string{ + defaultKRBDB: defaultKRBDB + ":9464", + "echo": "echo:9463", + "connector": "connector:9462", + "jaeger": "jaeger:8888", +} + +// writeObsFiles creates all observability config files in the current directory. +func writeObsFiles(opts *configOptions) error { + prometheusData := []byte(buildPrometheusYML(&opts.obsOpts)) + if err := writeObsFile("prometheus.yml", prometheusData); err != nil { + return err + } + + if err := os.MkdirAll("grafana", 0o750); err != nil { + return fmt.Errorf("failed to create grafana directory: %w", err) + } + + grafanaFiles := []struct { + path string + data []byte + }{ + {"grafana/grafana.ini", []byte(buildGrafanaINI(&opts.obsOpts))}, + {"grafana/grafana-datasources.yml", []byte(buildGrafanaDatasourcesYML())}, + {"grafana/grafana-dashboards.yml", []byte(buildGrafanaDashboardsYML())}, + {"grafana/prometheus.json", grafanaDashboardPrometheus}, + {"grafana/kerberos_runtime.json", grafanaDashboardRuntime}, + {"grafana/kerberos_http.json", grafanaDashboardHTTP}, + } + + for _, f := range grafanaFiles { + if err := writeObsFile(f.path, f.data); err != nil { + return err + } + } + + return writeObsFile("jaeger.yml", []byte(buildJaegerYML())) +} + +// writeObsFile writes data to path and prints a confirmation line. +func writeObsFile(path string, data []byte) error { + if err := os.WriteFile(path, data, 0o600); err != nil { + return fmt.Errorf("failed to write %s: %w", path, err) + } + + fmt.Fprintf(os.Stdout, "%s written\n", path) + + return nil +} + +// buildPrometheusYML generates a prometheus.yml with scrape configs for the selected targets. +func buildPrometheusYML(opts *obsConfigOptions) string { + var b strings.Builder + + b.WriteString("global:\n scrape_interval: 15s\n\nscrape_configs:\n") + + for _, target := range opts.scrapeTargets { + hostPort, ok := scrapeTargetPorts[target] + if !ok { + continue + } + + fmt.Fprintf(&b, + " - job_name: %s\n static_configs:\n - targets: [\"%s\"]\n", + target, hostPort, + ) + } + + return b.String() +} + +// buildGrafanaINI generates a slim grafana.ini with only the sections used in this deployment. +func buildGrafanaINI(opts *obsConfigOptions) string { + var b strings.Builder + + b.WriteString("[database]\n") + + if opts.grafanaDB == driverPostgres { + b.WriteString("type = postgres\n") + b.WriteString("host = postgres:5432\n") + b.WriteString("name = kerberos\n") + b.WriteString("user = kerberos\n") + b.WriteString("password = kerberos\n") + } else { + b.WriteString("type = sqlite3\n") + } + + b.WriteString("\n[auth.anonymous]\n") + + if opts.grafanaAnonymous { + b.WriteString("enabled = true\n") + b.WriteString("org_name = Main Org.\n") + b.WriteString("org_role = Viewer\n") + } else { + b.WriteString("enabled = false\n") + } + + return b.String() +} + +// buildGrafanaDatasourcesYML generates the Grafana datasource provisioning file. +func buildGrafanaDatasourcesYML() string { + return `apiVersion: 1 + +datasources: + - name: prometheus + type: prometheus + url: http://prometheus:9090 + uid: prometheus +` +} + +// buildGrafanaDashboardsYML generates the Grafana dashboard provisioning file. +func buildGrafanaDashboardsYML() string { + return `apiVersion: 1 + +providers: + - name: "Prometheus status" + type: file + options: + path: /var/lib/grafana/prometheus.json + - name: "Kerberos runtime" + type: file + options: + path: /var/lib/grafana/kerberos_runtime.json + - name: "Kerberos HTTP" + type: file + options: + path: /var/lib/grafana/kerberos_http.json +` +} + +// buildJaegerYML returns the static Jaeger configuration. +func buildJaegerYML() string { + return `service: + extensions: [jaeger_storage, jaeger_query] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [jaeger_storage_exporter] + telemetry: + resource: + service.name: jaeger + metrics: + level: detailed + readers: + - pull: + exporter: + prometheus: + host: 0.0.0.0 + port: 8888 + logs: + level: info + +extensions: + jaeger_query: + storage: + traces: badger_store + traces_archive: badger_archive + ui: + config_file: /config-ui.json + http: + endpoint: 0.0.0.0:16686 + grpc: + endpoint: 0.0.0.0:16685 + jaeger_storage: + backends: + badger_store: + badger: + directories: + keys: "/jaeger/" + values: "/jaeger/" + ephemeral: false + ttl: + spans: 48h + metrics_update_interval: 10s + badger_archive: + badger: + directories: + keys: "/jaeger/archive/" + values: "/jaeger/archive/" + ephemeral: false + ttl: + spans: 72h + metrics_update_interval: 10s + +receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + +processors: + batch: + +exporters: + jaeger_storage_exporter: + trace_storage: badger_store +` +} diff --git a/go.mod b/go.mod index 920de5c..844f741 100644 --- a/go.mod +++ b/go.mod @@ -27,18 +27,38 @@ require ( require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.6 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/huh v1.0.0 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v1.0.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/oasdiff/yaml v0.1.1 // indirect @@ -50,11 +70,13 @@ require ( github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.35.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.70.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 // indirect @@ -72,6 +94,7 @@ require ( go.opentelemetry.io/otel/sdk/log v0.21.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect golang.org/x/net v0.57.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect diff --git a/go.sum b/go.sum index 1c474a8..6b7dafc 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,37 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= +github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -16,6 +40,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo= github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -54,10 +80,24 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -87,6 +127,9 @@ github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+ github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= @@ -114,6 +157,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.70.0 h1:qU2CqTGdlstwoVhu1WfjJJ3z2ntcNjTJO0ksTsFKzPI= @@ -176,6 +221,7 @@ golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= From dea7ccf83ad91e8d2b35987b083cf0a8f61ecb05 Mon Sep 17 00:00:00 2001 From: maansaake Date: Mon, 17 Aug 2026 21:55:31 +0200 Subject: [PATCH 04/13] add workflows and makefile targets --- .github/workflows/callable-cli.yaml | 136 ++++++++++++++++++++++++++++ .github/workflows/main.yaml | 4 + .github/workflows/pull-request.yaml | 4 + Makefile | 53 +++++++++++ cmd/krbctl/cmd/root.go | 17 +++- 5 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/callable-cli.yaml diff --git a/.github/workflows/callable-cli.yaml b/.github/workflows/callable-cli.yaml new file mode 100644 index 0000000..1c92b23 --- /dev/null +++ b/.github/workflows/callable-cli.yaml @@ -0,0 +1,136 @@ +name: krbctl CLI + +on: + workflow_call: + release: + types: + - published + +concurrency: + group: cli-${{ github.event.pull_request.number || github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Golang + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - name: Compute build cache key + id: build-cache-key + run: | + checksum=$( + { + cat go.mod + [ -f go.sum ] && cat go.sum + cat Makefile + find . -type f -name '*.go' \ + -not -path './test/suites/*' \ + -not -path './tools/*' \ + -print0 | sort -z | xargs -0 cat + } | md5sum | awk '{print $1}' + ) + echo "checksum=$checksum" >> "$GITHUB_OUTPUT" + + - name: Cache Go module cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/go/pkg/mod + key: cli-build-go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} + + - name: Cache Go build cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/go-build + key: cli-build-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} + restore-keys: | + cli-build-go-build-${{ runner.os }}- + + - name: Build krbctl + run: make krbctl/build + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Golang + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - name: Compute build cache key + id: build-cache-key + run: | + checksum=$( + { + cat go.mod + [ -f go.sum ] && cat go.sum + cat Makefile + find . -type f -name '*.go' \ + -not -path './test/suites/*' \ + -not -path './tools/*' \ + -print0 | sort -z | xargs -0 cat + } | md5sum | awk '{print $1}' + ) + echo "checksum=$checksum" >> "$GITHUB_OUTPUT" + + - name: Cache Go module cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/go/pkg/mod + key: cli-test-go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} + + - name: Cache Go build cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/go-build + key: cli-test-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} + restore-keys: | + cli-test-go-build-${{ runner.os }}- + + - name: Test krbctl + run: make krbctl/test + + release-assets: + if: github.event_name == 'release' + needs: + - build + - test + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Golang + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - name: Build release artifacts + run: make krbctl/release VERSION=${{ github.ref_name }} + + - name: Upload artifacts to release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ github.ref_name }}" \ + build/release/*.tar.gz \ + build/release/*.zip \ + build/release/checksums.txt \ + --clobber diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e8476dc..dc2e62b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -20,6 +20,10 @@ jobs: uses: ./.github/workflows/callable-build.yaml secrets: inherit + cli: + uses: ./.github/workflows/callable-cli.yaml + secrets: inherit + image: uses: ./.github/workflows/callable-image.yaml secrets: inherit diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 0d2bf9c..b083bf9 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -27,3 +27,7 @@ jobs: unit-test: uses: ./.github/workflows/callable-test-unit.yaml secrets: inherit + + cli: + uses: ./.github/workflows/callable-cli.yaml + secrets: inherit diff --git a/Makefile b/Makefile index ccd39cb..c61b843 100644 --- a/Makefile +++ b/Makefile @@ -310,6 +310,59 @@ connector/docker/stop: connector/docker/rm: @docker rm connector || true +# krbctl version injection: the version string is compiled into the binary via +# ldflags, since krbctl is a distributed CLI and end users won't set env vars. +KRBCTL_PKG := github.com/trebent/kerberos/cmd/krbctl/cmd +KRBCTL_LDFLAGS := -s -w -X $(KRBCTL_PKG).version=$(VERSION) + +# krbctl release build matrix (GOOS/GOARCH pairs). +KRBCTL_PLATFORMS := \ + linux/amd64 \ + linux/arm64 \ + darwin/amd64 \ + darwin/arm64 \ + windows/amd64 \ + windows/arm64 + +krbctl/build: + $(call cecho,Building krbctl binary...,$(BOLD_YELLOW)) + @mkdir -p build + @CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="$(KRBCTL_LDFLAGS)" -o build/krbctl ./cmd/krbctl + +krbctl/install: + $(call cecho,Installing krbctl binary to $(GOBIN)...,$(BOLD_YELLOW)) + @CGO_ENABLED=0 GOOS=linux go install -trimpath -ldflags="$(KRBCTL_LDFLAGS)" ./cmd/krbctl + +krbctl/test: + $(call cecho,Running krbctl tests...,$(BOLD_YELLOW)) + @go test -v ./cmd/krbctl/... -failfast + +krbctl/release: + $(call cecho,Building krbctl release artifacts for version $(VERSION)...,$(BOLD_YELLOW)) + @rm -rf build/release + @mkdir -p build/release + @for platform in $(KRBCTL_PLATFORMS); do \ + os=$${platform%/*}; \ + arch=$${platform#*/}; \ + name=krbctl_$(VERSION)_$${os}_$${arch}; \ + bin=krbctl; \ + if [ "$${os}" = "windows" ]; then bin=krbctl.exe; fi; \ + printf "${BOLD_YELLOW} -> $${os}/$${arch}${RESET}\n"; \ + mkdir -p build/release/$${name}; \ + CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} \ + go build -trimpath -ldflags="$(KRBCTL_LDFLAGS)" \ + -o build/release/$${name}/$${bin} ./cmd/krbctl || exit 1; \ + if [ "$${os}" = "windows" ]; then \ + (cd build/release && zip -q -r $${name}.zip $${name}) || exit 1; \ + else \ + tar -czf build/release/$${name}.tar.gz -C build/release $${name} || exit 1; \ + fi; \ + rm -rf build/release/$${name}; \ + done + $(call cecho,Generating checksums...,$(BOLD_YELLOW)) + @cd build/release && sha256sum *.tar.gz *.zip > checksums.txt + $(call cecho,krbctl release artifacts written to build/release.,$(BOLD_GREEN)) + install/deps: go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.6.0 diff --git a/cmd/krbctl/cmd/root.go b/cmd/krbctl/cmd/root.go index 0c864b3..3019b7b 100644 --- a/cmd/krbctl/cmd/root.go +++ b/cmd/krbctl/cmd/root.go @@ -2,17 +2,26 @@ package cmd import ( + "fmt" + "github.com/spf13/cobra" ) +// version is the krbctl version. It defaults to "unset" and is overridden at +// build time via -ldflags "-X github.com/trebent/kerberos/cmd/krbctl/cmd.version=...". +var version = "unset" + // Execute adds all child commands to the root command and runs it. func Execute() error { root := &cobra.Command{ - Use: "krbctl", - Short: "krbctl is the Kerberos deployment CLI", - Long: `krbctl helps you set up a Kerberos deployment by generating + Use: "krbctl", + Version: version, + Short: "krbctl is the Kerberos deployment CLI", + Long: fmt.Sprintf(`krbctl helps you set up a Kerberos deployment by generating a base compose.yaml and a base kerberos configuration file through -interactive prompts.`, +interactive prompts. + +Version: %s`, version), } root.AddCommand(newComposeCmd()) From 0f0e2a443a68f748de4cd0f81035540d8eec6f3e Mon Sep 17 00:00:00 2001 From: maansaake Date: Mon, 17 Aug 2026 22:05:50 +0200 Subject: [PATCH 05/13] fix(ci): split krbctl release into standalone workflow The release-assets job in callable-cli.yaml requested contents: write, but callers (pull-request.yaml, main.yaml) only grant contents: read. A reusable workflow cannot request more permission than its caller grants, so GitHub rejected the whole invocation at startup (startup_failure). Move release asset building into a standalone release-cli.yaml triggered only on release: published (contents: write), and add Go module + build caching to that job. callable-cli.yaml now only builds and tests (contents: read). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/callable-cli.yaml | 34 -------------- .github/workflows/release-cli.yaml | 71 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/release-cli.yaml diff --git a/.github/workflows/callable-cli.yaml b/.github/workflows/callable-cli.yaml index 1c92b23..f2e4ae7 100644 --- a/.github/workflows/callable-cli.yaml +++ b/.github/workflows/callable-cli.yaml @@ -2,9 +2,6 @@ name: krbctl CLI on: workflow_call: - release: - types: - - published concurrency: group: cli-${{ github.event.pull_request.number || github.ref_name }} @@ -103,34 +100,3 @@ jobs: - name: Test krbctl run: make krbctl/test - - release-assets: - if: github.event_name == 'release' - needs: - - build - - test - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Set up Golang - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - cache: false - - - name: Build release artifacts - run: make krbctl/release VERSION=${{ github.ref_name }} - - - name: Upload artifacts to release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release upload "${{ github.ref_name }}" \ - build/release/*.tar.gz \ - build/release/*.zip \ - build/release/checksums.txt \ - --clobber diff --git a/.github/workflows/release-cli.yaml b/.github/workflows/release-cli.yaml new file mode 100644 index 0000000..cc5cf70 --- /dev/null +++ b/.github/workflows/release-cli.yaml @@ -0,0 +1,71 @@ +name: Release krbctl CLI + +on: + release: + types: + - published + +concurrency: + group: release-cli-${{ github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + release-assets: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Golang + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - name: Compute build cache key + id: build-cache-key + run: | + checksum=$( + { + cat go.mod + [ -f go.sum ] && cat go.sum + cat Makefile + find . -type f -name '*.go' \ + -not -path './test/suites/*' \ + -not -path './tools/*' \ + -print0 | sort -z | xargs -0 cat + } | md5sum | awk '{print $1}' + ) + echo "checksum=$checksum" >> "$GITHUB_OUTPUT" + + - name: Cache Go module cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/go/pkg/mod + key: cli-release-go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} + + - name: Cache Go build cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/go-build + key: cli-release-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} + restore-keys: | + cli-release-go-build-${{ runner.os }}- + + - name: Build release artifacts + run: make krbctl/release VERSION=${{ github.ref_name }} + + - name: Upload artifacts to release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ github.ref_name }}" \ + build/release/*.tar.gz \ + build/release/*.zip \ + build/release/checksums.txt \ + --clobber From 12a412127012fda8d449ee9f136dc6d12f121775 Mon Sep 17 00:00:00 2001 From: maansaake Date: Tue, 18 Aug 2026 07:00:17 +0200 Subject: [PATCH 06/13] update based on feedback --- cmd/krbctl/cmd/builders_test.go | 101 ++++++- cmd/krbctl/cmd/compose.go | 53 ++-- cmd/krbctl/cmd/config.go | 453 +++++++++++++++++++------------- cmd/krbctl/cmd/obsconfig.go | 4 +- 4 files changed, 396 insertions(+), 215 deletions(-) diff --git a/cmd/krbctl/cmd/builders_test.go b/cmd/krbctl/cmd/builders_test.go index 0830191..37b381f 100644 --- a/cmd/krbctl/cmd/builders_test.go +++ b/cmd/krbctl/cmd/builders_test.go @@ -42,12 +42,11 @@ func TestBuildConfig_BasicBackend(t *testing.T) { } } -func TestBuildConfig_IncludesObs(t *testing.T) { +func TestBuildConfig_AlwaysIncludesObs(t *testing.T) { t.Parallel() opts := &configOptions{ backends: []backendEntry{{name: "api", host: "api", port: 9000}}, - includeObs: true, persistenceMode: driverSQLite, } @@ -59,7 +58,62 @@ func TestBuildConfig_IncludesObs(t *testing.T) { } if _, ok := result["observability"]; !ok { - t.Error("expected observability section") + t.Error("expected observability section to always be present") + } +} + +func TestBuildConfig_PerBackendAuth(t *testing.T) { + t.Parallel() + + opts := &configOptions{ + backends: []backendEntry{ + {name: "secured", host: "secured", port: 8080, auth: true}, + {name: "open", host: "open", port: 8081, auth: false}, + }, + persistenceMode: driverSQLite, + } + + data, _ := buildConfig(opts) + + var result map[string]any + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + auth, ok := result["auth"].(map[string]any) + if !ok { + t.Fatal("expected auth section") + } + + scheme, _ := auth["scheme"].(map[string]any) + mappings, ok := scheme["mappings"].([]any) + if !ok || len(mappings) != 1 { + t.Fatalf("expected exactly 1 auth mapping, got %v", scheme["mappings"]) + } + + mapping, _ := mappings[0].(map[string]any) + if mapping["backend"] != "secured" { + t.Errorf("expected auth mapping for 'secured', got %v", mapping["backend"]) + } +} + +func TestBuildConfig_NoAuthWhenNoneEnabled(t *testing.T) { + t.Parallel() + + opts := &configOptions{ + backends: []backendEntry{{name: "open", host: "open", port: 8080, auth: false}}, + persistenceMode: driverSQLite, + } + + data, _ := buildConfig(opts) + + var result map[string]any + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + if _, ok := result["auth"]; ok { + t.Error("expected no auth section when no backend has auth enabled") } } @@ -109,12 +163,12 @@ func TestBuildPrometheusYML_AllTargets(t *testing.T) { t.Error("expected kerberos:9464") } - if !strings.Contains(yml, "echo:9463") { - t.Error("expected echo:9463") + if !strings.Contains(yml, "echo:9464") { + t.Error("expected echo:9464") } - if !strings.Contains(yml, "connector:9462") { - t.Error("expected connector:9462") + if !strings.Contains(yml, "connector:9464") { + t.Error("expected connector:9464") } if !strings.Contains(yml, "jaeger:8888") { @@ -238,6 +292,39 @@ func TestBuildJaegerYML(t *testing.T) { } } +// ---- buildCompose ---- + +func TestBuildCompose_NoEnvVarInjection(t *testing.T) { + t.Parallel() + + opts := &composeOptions{ + includeEcho: true, + includeObsStack: true, + includePostgres: true, + includeConnector: true, + } + + out := buildCompose(opts) + + if strings.Contains(out, "${") { + t.Error("compose output should not contain any ${...} env var injection") + } + + for _, want := range []string{ + "ghcr.io/trebent/kerberos:latest", + "- 30000:30000", + "- 30001:30001", + "- 15000:15000", + "- 30100:30100", + "- 9464:9464", + "OTEL_EXPORTER_PROMETHEUS_PORT=9464", + } { + if !strings.Contains(out, want) { + t.Errorf("expected compose output to contain %q", want) + } + } +} + // ---- buildConnectorJSON ---- func TestBuildConnectorJSON_SQLite(t *testing.T) { diff --git a/cmd/krbctl/cmd/compose.go b/cmd/krbctl/cmd/compose.go index d533b88..485c352 100644 --- a/cmd/krbctl/cmd/compose.go +++ b/cmd/krbctl/cmd/compose.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" ) @@ -46,20 +47,24 @@ func runCompose(cmd *cobra.Command, _ []string) error { huh.NewConfirm(). Title("Include the echo service?"). Description("Useful for testing backends."). + WithButtonAlignment(lipgloss.Left). Value(&opts.includeEcho), huh.NewConfirm(). Title("Include the observability stack?"). Description("Adds Prometheus, Grafana, and Jaeger services."). + WithButtonAlignment(lipgloss.Left). Value(&opts.includeObsStack), huh.NewConfirm(). Title("Include PostgreSQL as the persistence backend?"). Description("Uses SQLite by default if skipped."). + WithButtonAlignment(lipgloss.Left). Value(&opts.includePostgres), huh.NewConfirm(). Title("Include the admin-connector service?"). + WithButtonAlignment(lipgloss.Left). Value(&opts.includeConnector), ), ) @@ -119,7 +124,7 @@ func writePostgresService(b *strings.Builder, opts *composeOptions) { func writeKerberosService(b *strings.Builder, opts *composeOptions) { b.WriteString(` kerberos: - image: "ghcr.io/trebent/kerberos:${VERSION:-unset}" + image: "ghcr.io/trebent/kerberos:latest" command: --config /krb.json pull_policy: if_not_present `) @@ -133,23 +138,22 @@ func writeKerberosService(b *strings.Builder, opts *composeOptions) { b.WriteString(` restart: on-failure ports: - - ${KERBEROS_PORT:-30000}:${KERBEROS_PORT:-30000} - - ${KERBEROS_ADMIN_PORT:-30001}:${KERBEROS_ADMIN_PORT:-30001} + - 30000:30000 + - 30001:30001 `) if opts.includeObsStack { - b.WriteString(" - ${KERBEROS_METRICS_PORT:-9464}:${KERBEROS_METRICS_PORT:-9464}\n") + b.WriteString(" - 9464:9464\n") } b.WriteString(` environment: - LOG_TO_CONSOLE=1 - - LOG_VERBOSITY=${LOG_VERBOSITY:-20} - - PORT=${KERBEROS_PORT:-30000} - - ADMIN_PORT=${KERBEROS_ADMIN_PORT:-30001} - - VERSION=${VERSION:-unset} + - LOG_VERBOSITY=20 + - PORT=30000 + - ADMIN_PORT=30001 `) - writeOtelEnv(b, opts.includeObsStack, "kerberos", "${KERBEROS_METRICS_PORT:-9464}") + writeOtelEnv(b, opts.includeObsStack, "kerberos") b.WriteString(` volumes: - ./krb.json:/krb.json:ro @@ -163,22 +167,22 @@ func writeEchoService(b *strings.Builder, opts *composeOptions) { } b.WriteString(` echo: - image: "ghcr.io/trebent/kerberos/echo:${VERSION:-unset}" + image: "ghcr.io/trebent/kerberos/echo:latest" pull_policy: if_not_present restart: on-failure ports: - - ${ECHO_PORT:-15000}:${ECHO_PORT:-15000} + - 15000:15000 `) if opts.includeObsStack { - b.WriteString(" - ${ECHO_METRICS_PORT:-9463}:${ECHO_METRICS_PORT:-9463}\n") + b.WriteString(" - 9464:9464\n") } b.WriteString(` environment: - - PORT=${ECHO_PORT:-15000} + - PORT=15000 `) - writeOtelEnv(b, opts.includeObsStack, "echo", "${ECHO_METRICS_PORT:-9463}") + writeOtelEnv(b, opts.includeObsStack, "echo") b.WriteString("\n") } @@ -188,7 +192,7 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { } b.WriteString(` connector: - image: "ghcr.io/trebent/kerberos/admin-connector:${VERSION:-unset}" + image: "ghcr.io/trebent/kerberos/admin-connector:latest" command: --config /connector.json pull_policy: if_not_present depends_on: @@ -196,21 +200,20 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { condition: service_started restart: on-failure ports: - - ${CONNECTOR_PORT:-30100}:${CONNECTOR_PORT:-30100} + - 30100:30100 `) if opts.includeObsStack { - b.WriteString(" - ${CONNECTOR_METRICS_PORT:-9462}:${CONNECTOR_METRICS_PORT:-9462}\n") + b.WriteString(" - 9464:9464\n") } b.WriteString(` environment: - LOG_TO_CONSOLE=true - - LOG_VERBOSITY=${LOG_VERBOSITY:-20} - - VERSION=${VERSION:-unset} - - PORT=${CONNECTOR_PORT:-30100} + - LOG_VERBOSITY=20 + - PORT=30100 `) - writeOtelEnv(b, opts.includeObsStack, "connector", "${CONNECTOR_METRICS_PORT:-9462}") + writeOtelEnv(b, opts.includeObsStack, "connector") b.WriteString(` volumes: - ./connector.json:/connector.json:ro @@ -230,7 +233,7 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { "--storage.tsdb.retention.size", "1GB"] restart: on-failure ports: - - ${PROM_PORT:-9090}:9090 + - 9090:9090 volumes: - ./prometheus.yml:/prometheus.yml - prometheus:/prometheus @@ -240,7 +243,7 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { pull_policy: if_not_present restart: on-failure ports: - - ${GRAFANA_PORT:-3000}:3000 + - 3000:3000 volumes: - ./grafana/grafana.ini:/etc/grafana/grafana.ini - ./grafana/grafana-datasources.yml:/etc/grafana/provisioning/datasources/grafana-datasources.yml @@ -275,13 +278,13 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { `) } -func writeOtelEnv(b *strings.Builder, withObs bool, hostname, metricsPort string) { +func writeOtelEnv(b *strings.Builder, withObs bool, hostname string) { if withObs { fmt.Fprintf(b, " - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317\n") fmt.Fprintf(b, " - OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=grpc\n") fmt.Fprintf(b, " - OTEL_METRICS_EXPORTER=prometheus\n") fmt.Fprintf(b, " - OTEL_EXPORTER_PROMETHEUS_HOST=%s\n", hostname) - fmt.Fprintf(b, " - OTEL_EXPORTER_PROMETHEUS_PORT=%s\n", metricsPort) + fmt.Fprintf(b, " - OTEL_EXPORTER_PROMETHEUS_PORT=9464\n") } else { b.WriteString(" - OTEL_METRICS_EXPORTER=none\n") b.WriteString(" - OTEL_TRACES_EXPORTER=none\n") diff --git a/cmd/krbctl/cmd/config.go b/cmd/krbctl/cmd/config.go index 4ac61c1..920f979 100644 --- a/cmd/krbctl/cmd/config.go +++ b/cmd/krbctl/cmd/config.go @@ -2,12 +2,14 @@ package cmd import ( "encoding/json" + "errors" "fmt" "os" "strconv" "strings" "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" ) @@ -16,19 +18,21 @@ const ( driverSQLite = "sqlite" defaultKRBDB = "kerberos" defaultConnectorTarget = "http://kerberos:30001" + echoBackendName = "echo" + echoBackendHost = "echo" + echoBackendPort = 15000 ) // configOptions holds all answers collected from the interactive config session. type configOptions struct { // Kerberos gateway backends []backendEntry - includeAuth bool persistenceMode string // "sqlite" or "postgres" outputPath string - // Observability - includeObs bool - obsOpts obsConfigOptions + // Observability stack (Prometheus/Grafana/Jaeger) config generation + includeObsStack bool + obsOpts obsConfigOptions // Admin-connector includeConnector bool @@ -39,9 +43,10 @@ type backendEntry struct { name string host string port int + auth bool } -// obsConfigOptions holds the answers for the observability config section. +// obsConfigOptions holds the answers for the observability stack config section. type obsConfigOptions struct { scrapeTargets []string // e.g. ["kerberos","echo","connector","jaeger"] grafanaDB string // "postgres" or "sqlite" @@ -50,7 +55,6 @@ type obsConfigOptions struct { // connectorOptions holds the answers for the admin-connector config section. type connectorOptions struct { - targetURL string corsOrigin string persistenceMode string // "sqlite" or "postgres" } @@ -60,8 +64,10 @@ func newConfigCmd() *cobra.Command { Use: "config", Short: "Interactively generate a base Kerberos configuration file", Long: `Walks you through a series of prompts to build a base Kerberos JSON -configuration file. Mandatory sections are always included; optional sections -(auth, observability, postgres persistence, admin-connector) can be skipped.`, +configuration file. Mandatory sections are always included (Kerberos +observability is always enabled); optional sections (per-backend auth, +observability-stack config, postgres persistence, admin-connector) can be +skipped.`, RunE: runConfig, } @@ -79,30 +85,23 @@ func runConfig(cmd *cobra.Command, _ []string) error { opts := &configOptions{ outputPath: output, connectorOpts: connectorOptions{ - targetURL: defaultConnectorTarget, corsOrigin: defaultConnectorTarget, persistenceMode: driverSQLite, }, + persistenceMode: driverSQLite, obsOpts: obsConfigOptions{ + scrapeTargets: []string{defaultKRBDB}, grafanaDB: driverPostgres, grafanaAnonymous: true, }, } - if err := promptKerberosSection(opts); err != nil { + if err := promptBackends(opts); err != nil { return err } - if opts.includeObs { - if err := promptObsSection(opts); err != nil { - return err - } - } - - if opts.includeConnector { - if err := promptConnectorSection(opts); err != nil { - return err - } + if err := promptFixedSections(opts); err != nil { + return err } // Write krb.json @@ -117,8 +116,8 @@ func runConfig(cmd *cobra.Command, _ []string) error { fmt.Fprintf(os.Stdout, "krb.json written to %s\n", opts.outputPath) - // Write observability config files - if opts.includeObs { + // Write observability stack config files + if opts.includeObsStack { if err := writeObsFiles(opts); err != nil { return err } @@ -141,118 +140,31 @@ func runConfig(cmd *cobra.Command, _ []string) error { return nil } -// promptKerberosSection runs the main Kerberos gateway configuration prompts. -func promptKerberosSection(opts *configOptions) error { - if err := promptBackendsHuh(opts); err != nil { +// promptBackends collects one or more backend target entries using huh. The echo +// service can be registered as a router backend up front; when present the user +// may finish without registering any manual backend. +func promptBackends(opts *configOptions) error { + if err := promptEchoBackend(opts); err != nil { return err } - persistenceOpts := []huh.Option[string]{ - huh.NewOption("SQLite (default, file-based)", driverSQLite), - huh.NewOption("PostgreSQL", driverPostgres), - } - opts.persistenceMode = driverSQLite - - form := huh.NewForm( - huh.NewGroup( - huh.NewConfirm(). - Title("Include the auth section?"). - Description("Enables basic authentication for backend routes."). - Value(&opts.includeAuth), - - huh.NewConfirm(). - Title("Include the observability section?"). - Description("Enables metrics and tracing for Kerberos."). - Value(&opts.includeObs), - - huh.NewSelect[string](). - Title("Persistence backend"). - Options(persistenceOpts...). - Value(&opts.persistenceMode), - - huh.NewConfirm(). - Title("Include the admin-connector?"). - Description("Generates connector.json for the admin-connector service."). - Value(&opts.includeConnector), - ), - ) - - if err := form.Run(); err != nil { - return fmt.Errorf("prompt cancelled: %w", err) - } - - return nil -} - -// promptBackendsHuh collects one or more backend target entries using huh. -func promptBackendsHuh(opts *configOptions) error { for { - var ( - name string - host string - portStr string - ) - - form := huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title(fmt.Sprintf("Backend %d — name", len(opts.backends)+1)). - Description(`Press Enter with an empty name to finish (at least one required).`). - Placeholder("my-api"). - Value(&name), - ), - ) - - if err := form.Run(); err != nil { - return fmt.Errorf("prompt cancelled: %w", err) + name, err := promptBackendName(opts) + if err != nil { + return err } - name = strings.TrimSpace(name) if name == "" { - if len(opts.backends) == 0 { - fmt.Fprintln(os.Stderr, "At least one backend is required.") - continue - } - break } - detailForm := huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title(fmt.Sprintf("Backend %q — host", name)). - Placeholder("localhost"). - Value(&host), - - huh.NewInput(). - Title(fmt.Sprintf("Backend %q — port", name)). - Placeholder("8080"). - Value(&portStr), - ), - ) - - if err := detailForm.Run(); err != nil { - return fmt.Errorf("prompt cancelled: %w", err) - } - - if strings.TrimSpace(host) == "" { - host = "localhost" + if err := promptBackendDetails(opts, name); err != nil { + return err } - port := parsePort(portStr) - opts.backends = append(opts.backends, backendEntry{name: name, host: host, port: port}) - - var addAnother bool - confirmForm := huh.NewForm( - huh.NewGroup( - huh.NewConfirm(). - Title("Add another backend?"). - Value(&addAnother), - ), - ) - - if err := confirmForm.Run(); err != nil { - return fmt.Errorf("prompt cancelled: %w", err) + addAnother, err := promptAddAnother() + if err != nil { + return err } if !addAnother { @@ -263,84 +175,141 @@ func promptBackendsHuh(opts *configOptions) error { return nil } -// promptObsSection runs the observability configuration prompts. -func promptObsSection(opts *configOptions) error { - opts.obsOpts.scrapeTargets = []string{defaultKRBDB} +// promptEchoBackend asks whether to register the echo service as a backend. +func promptEchoBackend(opts *configOptions) error { + var useEcho bool - grafanaDBOpts := []huh.Option[string]{ - huh.NewOption("PostgreSQL", driverPostgres), - huh.NewOption("SQLite (Grafana default)", "sqlite3"), + echoForm := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Use the echo service as a backend?"). + Description(fmt.Sprintf( + "Registers echo (%s:%d) as a router backend.", + echoBackendHost, echoBackendPort, + )). + WithButtonAlignment(lipgloss.Left). + Value(&useEcho), + ), + ) + + if err := echoForm.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) } - scrapeOpts := []huh.Option[string]{ - huh.NewOption("kerberos (port 9464)", "kerberos"), - huh.NewOption("echo (port 9463)", "echo"), - huh.NewOption("connector (port 9462)", "connector"), - huh.NewOption("jaeger (port 8888)", "jaeger"), + if useEcho { + opts.backends = append(opts.backends, backendEntry{ + name: echoBackendName, + host: echoBackendHost, + port: echoBackendPort, + }) } - form := huh.NewForm( - huh.NewGroup( - huh.NewMultiSelect[string](). - Title("Prometheus scrape targets"). - Description("Select services that should expose metrics to Prometheus."). - Options(scrapeOpts...). - Value(&opts.obsOpts.scrapeTargets), + return nil +} - huh.NewSelect[string](). - Title("Grafana database backend"). - Options(grafanaDBOpts...). - Value(&opts.obsOpts.grafanaDB), +// promptBackendName asks for a backend name. An empty result signals the user is +// done. At least one backend is required, enforced via inline validation. +func promptBackendName(opts *configOptions) (string, error) { + var name string - huh.NewConfirm(). - Title("Enable Grafana anonymous access?"). - Description("Allows viewing dashboards without logging in."). - Value(&opts.obsOpts.grafanaAnonymous), + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title(fmt.Sprintf("Backend %d — name", len(opts.backends)+1)). + Description(`Press Enter with an empty name to finish (at least one required).`). + Placeholder("my-api"). + Validate(func(s string) error { + if strings.TrimSpace(s) == "" && len(opts.backends) == 0 { + return errors.New("at least one backend is required") + } + + return nil + }). + Value(&name), ), ) if err := form.Run(); err != nil { - return fmt.Errorf("prompt cancelled: %w", err) + return "", fmt.Errorf("prompt cancelled: %w", err) } - return nil + return strings.TrimSpace(name), nil } -// promptConnectorSection runs the admin-connector configuration prompts. -func promptConnectorSection(opts *configOptions) error { - persistenceOpts := []huh.Option[string]{ - huh.NewOption("SQLite (default, file-based)", driverSQLite), - huh.NewOption("PostgreSQL", driverPostgres), - } - opts.connectorOpts.persistenceMode = driverSQLite +// promptBackendDetails asks for the host and port of the named backend and +// appends it to the backend list. +func promptBackendDetails(opts *configOptions, name string) error { + var ( + host string + portStr string + ) - form := huh.NewForm( + detailForm := huh.NewForm( huh.NewGroup( huh.NewInput(). - Title("Kerberos admin target URL"). - Description("The URL at which the admin-connector can reach Kerberos."). - Placeholder(defaultConnectorTarget). - Value(&opts.connectorOpts.targetURL), + Title(fmt.Sprintf("Backend %q — host", name)). + Placeholder("localhost"). + Value(&host), huh.NewInput(). - Title("Allowed CORS origin"). - Description("The origin browsers are served from (used to allow cross-origin requests)."). - Placeholder(defaultConnectorTarget). - Value(&opts.connectorOpts.corsOrigin), - - huh.NewSelect[string](). - Title("Connector persistence backend"). - Options(persistenceOpts...). - Value(&opts.connectorOpts.persistenceMode), + Title(fmt.Sprintf("Backend %q — port", name)). + Placeholder("8080"). + Value(&portStr), ), ) - if err := form.Run(); err != nil { + if err := detailForm.Run(); err != nil { return fmt.Errorf("prompt cancelled: %w", err) } - if strings.TrimSpace(opts.connectorOpts.targetURL) == "" { - opts.connectorOpts.targetURL = defaultConnectorTarget + if strings.TrimSpace(host) == "" { + host = "localhost" + } + + opts.backends = append(opts.backends, backendEntry{ + name: name, + host: host, + port: parsePort(portStr), + }) + + return nil +} + +// promptAddAnother asks whether to register another backend. +func promptAddAnother() (bool, error) { + var addAnother bool + + confirmForm := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Add another backend?"). + WithButtonAlignment(lipgloss.Left). + Value(&addAnother), + ), + ) + + if err := confirmForm.Run(); err != nil { + return false, fmt.Errorf("prompt cancelled: %w", err) + } + + return addAnother, nil +} + +// promptFixedSections runs the remaining configuration prompts as a single +// multi-group form so the user can move back and forth between sections with +// shift+tab. Conditional groups are hidden until their toggle is enabled. +func promptFixedSections(opts *configOptions) error { + form := huh.NewForm( + buildAuthGroup(opts), + buildObsToggleGroup(opts), + buildObsOptionsGroup(opts), + buildPersistenceGroup(opts), + buildConnectorToggleGroup(opts), + buildConnectorConfigGroup(opts), + ) + + if err := form.Run(); err != nil { + return fmt.Errorf("prompt cancelled: %w", err) } if strings.TrimSpace(opts.connectorOpts.corsOrigin) == "" { @@ -350,6 +319,118 @@ func promptConnectorSection(opts *configOptions) error { return nil } +// buildAuthGroup builds a dedicated authentication section with a per-backend +// toggle enabling basic auth for that backend. +func buildAuthGroup(opts *configOptions) *huh.Group { + fields := make([]huh.Field, 0, len(opts.backends)) + for i := range opts.backends { + fields = append(fields, huh.NewConfirm(). + Title(fmt.Sprintf("Enable basic auth for backend %q?", opts.backends[i].name)). + WithButtonAlignment(lipgloss.Left). + Value(&opts.backends[i].auth)) + } + + return huh.NewGroup(fields...).Title("Authentication") +} + +// buildObsToggleGroup builds the dedicated toggle controlling generation of the +// observability stack (Prometheus/Grafana/Jaeger) config files. +func buildObsToggleGroup(opts *configOptions) *huh.Group { + return huh.NewGroup( + huh.NewConfirm(). + Title("Generate observability stack config?"). + Description("Writes Prometheus, Grafana, and Jaeger config files for the " + + "observability stack. (Kerberos observability itself is always enabled.)"). + WithButtonAlignment(lipgloss.Left). + Value(&opts.includeObsStack), + ).Title("Observability stack") +} + +// buildObsOptionsGroup builds the observability stack detail options, hidden +// unless the observability stack toggle is enabled. +func buildObsOptionsGroup(opts *configOptions) *huh.Group { + grafanaDBOpts := []huh.Option[string]{ + huh.NewOption("PostgreSQL", driverPostgres), + huh.NewOption("SQLite (Grafana default)", "sqlite3"), + } + + scrapeOpts := []huh.Option[string]{ + huh.NewOption("kerberos (port 9464)", "kerberos"), + huh.NewOption("echo (port 9464)", "echo"), + huh.NewOption("connector (port 9464)", "connector"), + huh.NewOption("jaeger (port 8888)", "jaeger"), + } + + return huh.NewGroup( + huh.NewMultiSelect[string](). + Title("Prometheus scrape targets"). + Description("Select services that should expose metrics to Prometheus."). + Options(scrapeOpts...). + Value(&opts.obsOpts.scrapeTargets), + + huh.NewSelect[string](). + Title("Grafana database backend"). + Options(grafanaDBOpts...). + Value(&opts.obsOpts.grafanaDB), + + huh.NewConfirm(). + Title("Enable Grafana anonymous access?"). + Description("Allows viewing dashboards without logging in."). + WithButtonAlignment(lipgloss.Left). + Value(&opts.obsOpts.grafanaAnonymous), + ).Title("Observability stack options"). + WithHideFunc(func() bool { return !opts.includeObsStack }) +} + +// buildPersistenceGroup builds the Kerberos persistence selection section. +func buildPersistenceGroup(opts *configOptions) *huh.Group { + persistenceOpts := []huh.Option[string]{ + huh.NewOption("SQLite (default, file-based)", driverSQLite), + huh.NewOption("PostgreSQL", driverPostgres), + } + + return huh.NewGroup( + huh.NewSelect[string](). + Title("Persistence backend"). + Options(persistenceOpts...). + Value(&opts.persistenceMode), + ).Title("Persistence") +} + +// buildConnectorToggleGroup builds the dedicated admin-connector on/off section. +func buildConnectorToggleGroup(opts *configOptions) *huh.Group { + return huh.NewGroup( + huh.NewConfirm(). + Title("Include the admin-connector?"). + Description("Generates connector.json for the admin-connector service."). + WithButtonAlignment(lipgloss.Left). + Value(&opts.includeConnector), + ).Title("Admin-connector") +} + +// buildConnectorConfigGroup builds the admin-connector config detail section, +// hidden unless the admin-connector toggle is enabled. +func buildConnectorConfigGroup(opts *configOptions) *huh.Group { + persistenceOpts := []huh.Option[string]{ + huh.NewOption("SQLite (default, file-based)", driverSQLite), + huh.NewOption("PostgreSQL", driverPostgres), + } + + return huh.NewGroup( + huh.NewInput(). + Title("Allowed CORS origin"). + Description("The origin browsers are served from (used to allow cross-origin requests)."). + Placeholder(defaultConnectorTarget). + Value(&opts.connectorOpts.corsOrigin), + + huh.NewSelect[string](). + Title("Connector persistence backend"). + Options(persistenceOpts...). + Value(&opts.connectorOpts.persistenceMode), + ).Title("Admin-connector config"). + WithHideFunc(func() bool { return !opts.includeConnector }) +} + func parsePort(raw string) int { const defaultPort = 8080 @@ -380,15 +461,13 @@ func buildConfig(opts *configOptions) ([]byte, error) { }, } - if opts.includeObs { - root["observability"] = map[string]any{ - "enabled": true, - "runtimeMetrics": true, - } + root["observability"] = map[string]any{ + "enabled": true, + "runtimeMetrics": true, } - if opts.includeAuth && len(opts.backends) > 0 { - root["auth"] = buildAuthSection(opts.backends) + if authBackends := authEnabledBackends(opts.backends); len(authBackends) > 0 { + root["auth"] = buildAuthSection(authBackends) } root["persistence"] = buildPersistenceSection(opts.persistenceMode) @@ -396,6 +475,18 @@ func buildConfig(opts *configOptions) ([]byte, error) { return json.MarshalIndent(root, "", " ") } +// authEnabledBackends returns the subset of backends that have auth enabled. +func authEnabledBackends(backends []backendEntry) []backendEntry { + enabled := make([]backendEntry, 0, len(backends)) + for _, b := range backends { + if b.auth { + enabled = append(enabled, b) + } + } + + return enabled +} + func buildAuthSection(backends []backendEntry) map[string]any { mappings := make([]map[string]any, 0, len(backends)) for _, b := range backends { diff --git a/cmd/krbctl/cmd/obsconfig.go b/cmd/krbctl/cmd/obsconfig.go index e57a38a..b7fc6de 100644 --- a/cmd/krbctl/cmd/obsconfig.go +++ b/cmd/krbctl/cmd/obsconfig.go @@ -19,8 +19,8 @@ var grafanaDashboardHTTP []byte // scrapeTargetPorts maps each known scrape target to its default host:port. var scrapeTargetPorts = map[string]string{ defaultKRBDB: defaultKRBDB + ":9464", - "echo": "echo:9463", - "connector": "connector:9462", + "echo": "echo:9464", + "connector": "connector:9464", "jaeger": "jaeger:8888", } From 3751bbb27b50b6c77583db991f881679e5f1d9ae Mon Sep 17 00:00:00 2001 From: maansaake Date: Tue, 18 Aug 2026 07:28:12 +0200 Subject: [PATCH 07/13] feat(krbctl): refine interactive compose and config UX Compose/config form and generation improvements: - Left-align huh confirm buttons for consistent yes/no rows - Combine config's fixed prompts into a single navigable multi-group form (shift+tab back/forth); keep the dynamic backend loop as its own step - Replace plain-text "at least one backend" output with inline huh validation - Offer echo as a router backend up front (echo:15000); allow finishing without manual backends when echo is registered - Per-backend basic auth toggle instead of a single global auth section - Always enable Kerberos observability; keep obs-STACK file generation as a separate, clearly-labeled optional section - Split admin-connector on/off into its own section; drop the unused admin-target-URL prompt - Replace connector origin input with an allow/deny-all CORS confirm - Dynamic Prometheus scrape targets: kerberos + jaeger + registered backends (backends scraped at :9464), connector auto-added when included - Hard-code compose ports/versions; drop all env-var injection; use the default Prometheus port 9464 for every service - Trim compose host port publishing to kerberos gw/admin, grafana, jaeger - Add a shared krbdata volume (/data/krb.db) for Kerberos and the connector when Postgres is disabled, with matching persistence config - Set LOG_VERBOSITY=0 for services that configure it - Update unit tests to cover the new behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cmd/krbctl/cmd/builders_test.go | 135 ++++++++++++++++++++---------- cmd/krbctl/cmd/compose.go | 50 +++++------ cmd/krbctl/cmd/config.go | 66 +++++++++------ cmd/krbctl/cmd/connectorconfig.go | 12 +-- cmd/krbctl/cmd/obsconfig.go | 64 ++++++++++---- 5 files changed, 208 insertions(+), 119 deletions(-) diff --git a/cmd/krbctl/cmd/builders_test.go b/cmd/krbctl/cmd/builders_test.go index 37b381f..9464feb 100644 --- a/cmd/krbctl/cmd/builders_test.go +++ b/cmd/krbctl/cmd/builders_test.go @@ -142,55 +142,70 @@ func TestBuildConfig_PostgresPersistence(t *testing.T) { } } -// ---- buildPrometheusYML ---- +// ---- resolveScrapeJobs / buildPrometheusYML ---- -func TestBuildPrometheusYML_AllTargets(t *testing.T) { +func TestResolveScrapeJobs_BackendsAndConnector(t *testing.T) { t.Parallel() - opts := &obsConfigOptions{ - scrapeTargets: []string{"kerberos", "echo", "connector", "jaeger"}, + opts := &configOptions{ + backends: []backendEntry{ + {name: "echo", host: "echo", port: 15000}, + {name: "api", host: "api-host", port: 8080}, + }, + includeConnector: true, + obsOpts: obsConfigOptions{ + scrapeTargets: []string{"kerberos", "jaeger", "echo", "api"}, + }, } - yml := buildPrometheusYML(opts) + yml := buildPrometheusYML(resolveScrapeJobs(opts)) - for _, target := range opts.scrapeTargets { - if !strings.Contains(yml, target) { - t.Errorf("expected target %q in prometheus.yml", target) + for _, want := range []string{ + "kerberos:9464", + "jaeger:8888", + "echo:9464", + "api-host:9464", + "connector:9464", + } { + if !strings.Contains(yml, want) { + t.Errorf("expected %q in prometheus.yml, got:\n%s", want, yml) } } +} - if !strings.Contains(yml, "kerberos:9464") { - t.Error("expected kerberos:9464") - } +func TestResolveScrapeJobs_NoEchoWhenNotRegistered(t *testing.T) { + t.Parallel() - if !strings.Contains(yml, "echo:9464") { - t.Error("expected echo:9464") + opts := &configOptions{ + backends: []backendEntry{{name: "api", host: "api", port: 8080}}, + obsOpts: obsConfigOptions{ + scrapeTargets: []string{"kerberos", "echo", "api"}, + }, } - if !strings.Contains(yml, "connector:9464") { - t.Error("expected connector:9464") + yml := buildPrometheusYML(resolveScrapeJobs(opts)) + + if strings.Contains(yml, "echo") { + t.Errorf("echo should not appear when it was not registered as a backend:\n%s", yml) } - if !strings.Contains(yml, "jaeger:8888") { - t.Error("expected jaeger:8888") + if !strings.Contains(yml, "api:9464") { + t.Error("expected api:9464") } } -func TestBuildPrometheusYML_SelectedTargets(t *testing.T) { +func TestResolveScrapeJobs_ConnectorOnlyWhenIncluded(t *testing.T) { t.Parallel() - opts := &obsConfigOptions{ - scrapeTargets: []string{"kerberos"}, + opts := &configOptions{ + includeConnector: false, + obsOpts: obsConfigOptions{scrapeTargets: []string{"kerberos"}}, } - yml := buildPrometheusYML(opts) + yml := buildPrometheusYML(resolveScrapeJobs(opts)) - if !strings.Contains(yml, "kerberos:9464") { - t.Error("expected kerberos:9464") - } - - if strings.Contains(yml, "echo") { - t.Error("echo should not be in output") + if strings.Contains(yml, "connector") { + t.Errorf("connector should not be scraped when not included:\n%s", yml) } } @@ -300,7 +315,7 @@ func TestBuildCompose_NoEnvVarInjection(t *testing.T) { opts := &composeOptions{ includeEcho: true, includeObsStack: true, - includePostgres: true, + includePostgres: false, includeConnector: true, } @@ -314,15 +329,38 @@ func TestBuildCompose_NoEnvVarInjection(t *testing.T) { "ghcr.io/trebent/kerberos:latest", "- 30000:30000", "- 30001:30001", - "- 15000:15000", - "- 30100:30100", - "- 9464:9464", - "OTEL_EXPORTER_PROMETHEUS_PORT=9464", + "- 3000:3000", + "- 16686:16686", + "LOG_VERBOSITY=0", + "krbdata:/data", } { if !strings.Contains(out, want) { t.Errorf("expected compose output to contain %q", want) } } + + // Only kerberos gw/admin, grafana and jaeger publish host ports. + for _, notWant := range []string{ + "- 9464:9464", + "- 15000:15000", + "- 30100:30100", + "- 9090:9090", + "LOG_VERBOSITY=20", + } { + if strings.Contains(out, notWant) { + t.Errorf("compose output should not contain %q", notWant) + } + } +} + +func TestBuildCompose_SharedSqliteVolumeOmittedWithPostgres(t *testing.T) { + t.Parallel() + + out := buildCompose(&composeOptions{includeConnector: true, includePostgres: true}) + + if strings.Contains(out, "krbdata") { + t.Errorf("krbdata volume should not be present when Postgres is enabled:\n%s", out) + } } // ---- buildConnectorJSON ---- @@ -331,7 +369,7 @@ func TestBuildConnectorJSON_SQLite(t *testing.T) { t.Parallel() opts := &connectorOptions{ - corsOrigin: "http://localhost:3000", + allowAllOrigins: true, persistenceMode: driverSQLite, } @@ -350,13 +388,12 @@ func TestBuildConnectorJSON_SQLite(t *testing.T) { t.Fatal("missing origins section") } - allowed, ok := origins["allowedOrigins"].([]any) - if !ok || len(allowed) == 0 { - t.Fatal("expected at least one allowed origin") + if origins["allowAll"] != true { + t.Errorf("expected allowAll=true, got %v", origins["allowAll"]) } - if allowed[0] != "http://localhost:3000" { - t.Errorf("expected origin http://localhost:3000, got %v", allowed[0]) + if _, ok := origins["denyAll"]; ok { + t.Error("did not expect denyAll when allowAll is set") } persistence, ok := result["persistence"].(map[string]any) @@ -367,13 +404,17 @@ func TestBuildConnectorJSON_SQLite(t *testing.T) { if persistence["driver"] != driverSQLite { t.Errorf("expected driver=sqlite, got %v", persistence["driver"]) } + + if persistence["address"] != sqliteSharedPath { + t.Errorf("expected shared sqlite address %q, got %v", sqliteSharedPath, persistence["address"]) + } } -func TestBuildConnectorJSON_DefaultOrigin(t *testing.T) { +func TestBuildConnectorJSON_DenyAll(t *testing.T) { t.Parallel() opts := &connectorOptions{ - corsOrigin: "", + allowAllOrigins: false, persistenceMode: driverSQLite, } @@ -382,7 +423,17 @@ func TestBuildConnectorJSON_DefaultOrigin(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - if !strings.Contains(string(data), "http://kerberos:30001") { - t.Error("expected default origin http://kerberos:30001") + var result map[string]any + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + origins, _ := result["origins"].(map[string]any) + if origins["denyAll"] != true { + t.Errorf("expected denyAll=true, got %v", origins["denyAll"]) + } + + if _, ok := origins["allowAll"]; ok { + t.Error("did not expect allowAll when denyAll is set") } } diff --git a/cmd/krbctl/cmd/compose.go b/cmd/krbctl/cmd/compose.go index 485c352..6946e6b 100644 --- a/cmd/krbctl/cmd/compose.go +++ b/cmd/krbctl/cmd/compose.go @@ -140,15 +140,9 @@ func writeKerberosService(b *strings.Builder, opts *composeOptions) { ports: - 30000:30000 - 30001:30001 -`) - - if opts.includeObsStack { - b.WriteString(" - 9464:9464\n") - } - - b.WriteString(` environment: + environment: - LOG_TO_CONSOLE=1 - - LOG_VERBOSITY=20 + - LOG_VERBOSITY=0 - PORT=30000 - ADMIN_PORT=30001 `) @@ -157,8 +151,13 @@ func writeKerberosService(b *strings.Builder, opts *composeOptions) { b.WriteString(` volumes: - ./krb.json:/krb.json:ro - `) + + if !opts.includePostgres { + b.WriteString(" - krbdata:/data\n") + } + + b.WriteString("\n") } func writeEchoService(b *strings.Builder, opts *composeOptions) { @@ -170,15 +169,7 @@ func writeEchoService(b *strings.Builder, opts *composeOptions) { image: "ghcr.io/trebent/kerberos/echo:latest" pull_policy: if_not_present restart: on-failure - ports: - - 15000:15000 -`) - - if opts.includeObsStack { - b.WriteString(" - 9464:9464\n") - } - - b.WriteString(` environment: + environment: - PORT=15000 `) @@ -199,17 +190,9 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { kerberos: condition: service_started restart: on-failure - ports: - - 30100:30100 -`) - - if opts.includeObsStack { - b.WriteString(" - 9464:9464\n") - } - - b.WriteString(` environment: + environment: - LOG_TO_CONSOLE=true - - LOG_VERBOSITY=20 + - LOG_VERBOSITY=0 - PORT=30100 `) @@ -217,8 +200,13 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { b.WriteString(` volumes: - ./connector.json:/connector.json:ro - `) + + if !opts.includePostgres { + b.WriteString(" - krbdata:/data\n") + } + + b.WriteString("\n") } func writeObsServices(b *strings.Builder, opts *composeOptions) { @@ -232,8 +220,6 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { command: ["--config.file=/prometheus.yml", "--storage.tsdb.path", "/prometheus/data", "--storage.tsdb.retention.size", "1GB"] restart: on-failure - ports: - - 9090:9090 volumes: - ./prometheus.yml:/prometheus.yml - prometheus:/prometheus @@ -296,6 +282,8 @@ func writeVolumes(b *strings.Builder, opts *composeOptions) { if opts.includePostgres { volumes = append(volumes, " postgres:") + } else { + volumes = append(volumes, " krbdata:") } if opts.includeObsStack { diff --git a/cmd/krbctl/cmd/config.go b/cmd/krbctl/cmd/config.go index 920f979..eacdd1d 100644 --- a/cmd/krbctl/cmd/config.go +++ b/cmd/krbctl/cmd/config.go @@ -14,13 +14,20 @@ import ( ) const ( - driverPostgres = "postgres" - driverSQLite = "sqlite" - defaultKRBDB = "kerberos" - defaultConnectorTarget = "http://kerberos:30001" - echoBackendName = "echo" - echoBackendHost = "echo" - echoBackendPort = 15000 + driverPostgres = "postgres" + driverSQLite = "sqlite" + defaultKRBDB = "kerberos" + echoBackendName = "echo" + echoBackendHost = "echo" + echoBackendPort = 15000 + jaegerName = "jaeger" + + // scrapeMetricsPort is the default Prometheus exporter port every service + // exposes its metrics on. + scrapeMetricsPort = 9464 + // sqliteSharedPath is the SQLite file location on the shared krbdata volume, + // letting Kerberos and the admin-connector share one database file. + sqliteSharedPath = "/data/krb.db" ) // configOptions holds all answers collected from the interactive config session. @@ -55,7 +62,7 @@ type obsConfigOptions struct { // connectorOptions holds the answers for the admin-connector config section. type connectorOptions struct { - corsOrigin string + allowAllOrigins bool persistenceMode string // "sqlite" or "postgres" } @@ -85,12 +92,11 @@ func runConfig(cmd *cobra.Command, _ []string) error { opts := &configOptions{ outputPath: output, connectorOpts: connectorOptions{ - corsOrigin: defaultConnectorTarget, + allowAllOrigins: true, persistenceMode: driverSQLite, }, persistenceMode: driverSQLite, obsOpts: obsConfigOptions{ - scrapeTargets: []string{defaultKRBDB}, grafanaDB: driverPostgres, grafanaAnonymous: true, }, @@ -299,6 +305,8 @@ func promptAddAnother() (bool, error) { // multi-group form so the user can move back and forth between sections with // shift+tab. Conditional groups are hidden until their toggle is enabled. func promptFixedSections(opts *configOptions) error { + opts.obsOpts.scrapeTargets = defaultScrapeTargets(opts) + form := huh.NewForm( buildAuthGroup(opts), buildObsToggleGroup(opts), @@ -312,11 +320,18 @@ func promptFixedSections(opts *configOptions) error { return fmt.Errorf("prompt cancelled: %w", err) } - if strings.TrimSpace(opts.connectorOpts.corsOrigin) == "" { - opts.connectorOpts.corsOrigin = defaultConnectorTarget + return nil +} + +// defaultScrapeTargets returns the scrape targets pre-selected by default: +// kerberos, jaeger, and every registered router backend. +func defaultScrapeTargets(opts *configOptions) []string { + targets := []string{defaultKRBDB, jaegerName} + for _, b := range opts.backends { + targets = append(targets, b.name) } - return nil + return targets } // buildAuthGroup builds a dedicated authentication section with a per-backend @@ -355,16 +370,19 @@ func buildObsOptionsGroup(opts *configOptions) *huh.Group { } scrapeOpts := []huh.Option[string]{ - huh.NewOption("kerberos (port 9464)", "kerberos"), - huh.NewOption("echo (port 9464)", "echo"), - huh.NewOption("connector (port 9464)", "connector"), - huh.NewOption("jaeger (port 8888)", "jaeger"), + huh.NewOption(fmt.Sprintf("kerberos (port %d)", scrapeMetricsPort), defaultKRBDB), + huh.NewOption("jaeger (port 8888)", jaegerName), + } + for _, b := range opts.backends { + scrapeOpts = append(scrapeOpts, huh.NewOption( + fmt.Sprintf("%s (%s:%d)", b.name, b.host, scrapeMetricsPort), b.name)) } return huh.NewGroup( huh.NewMultiSelect[string](). Title("Prometheus scrape targets"). - Description("Select services that should expose metrics to Prometheus."). + Description("Select services that should expose metrics to Prometheus. "+ + "The admin-connector is scraped automatically when included."). Options(scrapeOpts...). Value(&opts.obsOpts.scrapeTargets), @@ -417,11 +435,11 @@ func buildConnectorConfigGroup(opts *configOptions) *huh.Group { } return huh.NewGroup( - huh.NewInput(). - Title("Allowed CORS origin"). - Description("The origin browsers are served from (used to allow cross-origin requests)."). - Placeholder(defaultConnectorTarget). - Value(&opts.connectorOpts.corsOrigin), + huh.NewConfirm(). + Title("Allow all CORS origins?"). + Description("Yes allows any origin; No denies all cross-origin requests."). + WithButtonAlignment(lipgloss.Left). + Value(&opts.connectorOpts.allowAllOrigins), huh.NewSelect[string](). Title("Connector persistence backend"). @@ -524,6 +542,6 @@ func buildPersistenceSection(mode string) map[string]any { return map[string]any{ "driver": driverSQLite, - "address": "krb.db", + "address": sqliteSharedPath, } } diff --git a/cmd/krbctl/cmd/connectorconfig.go b/cmd/krbctl/cmd/connectorconfig.go index 14af24c..1d95904 100644 --- a/cmd/krbctl/cmd/connectorconfig.go +++ b/cmd/krbctl/cmd/connectorconfig.go @@ -7,15 +7,15 @@ import ( // buildConnectorJSON generates a minimal connector.json. func buildConnectorJSON(opts *connectorOptions) ([]byte, error) { - origin := opts.corsOrigin - if origin == "" { - origin = defaultConnectorTarget + origins := map[string]any{} + if opts.allowAllOrigins { + origins["allowAll"] = true + } else { + origins["denyAll"] = true } root := map[string]any{ - "origins": map[string]any{ - "allowedOrigins": []string{origin}, - }, + "origins": origins, "persistence": buildPersistenceSection(opts.persistenceMode), } diff --git a/cmd/krbctl/cmd/obsconfig.go b/cmd/krbctl/cmd/obsconfig.go index b7fc6de..9d0431a 100644 --- a/cmd/krbctl/cmd/obsconfig.go +++ b/cmd/krbctl/cmd/obsconfig.go @@ -16,17 +16,15 @@ var grafanaDashboardRuntime []byte //go:embed assets/grafana/kerberos_http.json var grafanaDashboardHTTP []byte -// scrapeTargetPorts maps each known scrape target to its default host:port. -var scrapeTargetPorts = map[string]string{ - defaultKRBDB: defaultKRBDB + ":9464", - "echo": "echo:9464", - "connector": "connector:9464", - "jaeger": "jaeger:8888", +// scrapeJob is a resolved Prometheus scrape target (job name + host:port). +type scrapeJob struct { + name string + target string } // writeObsFiles creates all observability config files in the current directory. func writeObsFiles(opts *configOptions) error { - prometheusData := []byte(buildPrometheusYML(&opts.obsOpts)) + prometheusData := []byte(buildPrometheusYML(resolveScrapeJobs(opts))) if err := writeObsFile("prometheus.yml", prometheusData); err != nil { return err } @@ -67,21 +65,55 @@ func writeObsFile(path string, data []byte) error { return nil } -// buildPrometheusYML generates a prometheus.yml with scrape configs for the selected targets. -func buildPrometheusYML(opts *obsConfigOptions) string { +// resolveScrapeJobs turns the selected scrape targets into concrete host:port +// jobs. kerberos and jaeger resolve to their well-known endpoints, every other +// selected target is treated as a registered router backend scraped on the +// default metrics port. The admin-connector is added automatically when the +// connector is included in the deployment. +func resolveScrapeJobs(opts *configOptions) []scrapeJob { + hosts := make(map[string]string, len(opts.backends)) + for _, b := range opts.backends { + hosts[b.name] = b.host + } + + jobs := make([]scrapeJob, 0, len(opts.obsOpts.scrapeTargets)+1) + + for _, target := range opts.obsOpts.scrapeTargets { + switch target { + case defaultKRBDB: + jobs = append( + jobs, + scrapeJob{defaultKRBDB, fmt.Sprintf("%s:%d", defaultKRBDB, scrapeMetricsPort)}, + ) + case jaegerName: + jobs = append(jobs, scrapeJob{jaegerName, "jaeger:8888"}) + default: + host, ok := hosts[target] + if !ok { + continue + } + + jobs = append(jobs, scrapeJob{target, fmt.Sprintf("%s:%d", host, scrapeMetricsPort)}) + } + } + + if opts.includeConnector { + jobs = append(jobs, scrapeJob{"connector", fmt.Sprintf("connector:%d", scrapeMetricsPort)}) + } + + return jobs +} + +// buildPrometheusYML generates a prometheus.yml with scrape configs for the given jobs. +func buildPrometheusYML(jobs []scrapeJob) string { var b strings.Builder b.WriteString("global:\n scrape_interval: 15s\n\nscrape_configs:\n") - for _, target := range opts.scrapeTargets { - hostPort, ok := scrapeTargetPorts[target] - if !ok { - continue - } - + for _, job := range jobs { fmt.Fprintf(&b, " - job_name: %s\n static_configs:\n - targets: [\"%s\"]\n", - target, hostPort, + job.name, job.target, ) } From 597badda2320d77c425f883ee7ab1eb5952b4ac7 Mon Sep 17 00:00:00 2001 From: maansaake Date: Sat, 22 Aug 2026 12:44:08 +0200 Subject: [PATCH 08/13] align sqlite usage and init volume permissions --- cmd/krbctl/cmd/assets/jaeger/config-ui.json | 3 + cmd/krbctl/cmd/builders_test.go | 439 -------------------- cmd/krbctl/cmd/compose.go | 86 +++- cmd/krbctl/cmd/config.go | 58 +-- cmd/krbctl/cmd/connectorconfig.go | 4 +- cmd/krbctl/cmd/obsconfig.go | 54 ++- 6 files changed, 135 insertions(+), 509 deletions(-) create mode 100644 cmd/krbctl/cmd/assets/jaeger/config-ui.json delete mode 100644 cmd/krbctl/cmd/builders_test.go diff --git a/cmd/krbctl/cmd/assets/jaeger/config-ui.json b/cmd/krbctl/cmd/assets/jaeger/config-ui.json new file mode 100644 index 0000000..9b8681b --- /dev/null +++ b/cmd/krbctl/cmd/assets/jaeger/config-ui.json @@ -0,0 +1,3 @@ +{ + "archiveEnabled": true +} \ No newline at end of file diff --git a/cmd/krbctl/cmd/builders_test.go b/cmd/krbctl/cmd/builders_test.go deleted file mode 100644 index 9464feb..0000000 --- a/cmd/krbctl/cmd/builders_test.go +++ /dev/null @@ -1,439 +0,0 @@ -package cmd - -import ( - "encoding/json" - "strings" - "testing" -) - -// ---- buildConfig ---- - -func TestBuildConfig_BasicBackend(t *testing.T) { - t.Parallel() - - opts := &configOptions{ - backends: []backendEntry{{name: "api", host: "localhost", port: 8080}}, - persistenceMode: driverSQLite, - } - - data, err := buildConfig(opts) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - var result map[string]any - if err := json.Unmarshal(data, &result); err != nil { - t.Fatalf("invalid JSON: %v", err) - } - - gw, ok := result["gateway"].(map[string]any) - if !ok { - t.Fatal("missing gateway section") - } - - router, ok := gw["router"].(map[string]any) - if !ok { - t.Fatal("missing router section") - } - - backends, ok := router["backends"].([]any) - if !ok || len(backends) != 1 { - t.Fatalf("expected 1 backend, got %v", router["backends"]) - } -} - -func TestBuildConfig_AlwaysIncludesObs(t *testing.T) { - t.Parallel() - - opts := &configOptions{ - backends: []backendEntry{{name: "api", host: "api", port: 9000}}, - persistenceMode: driverSQLite, - } - - data, _ := buildConfig(opts) - - var result map[string]any - if err := json.Unmarshal(data, &result); err != nil { - t.Fatalf("invalid JSON: %v", err) - } - - if _, ok := result["observability"]; !ok { - t.Error("expected observability section to always be present") - } -} - -func TestBuildConfig_PerBackendAuth(t *testing.T) { - t.Parallel() - - opts := &configOptions{ - backends: []backendEntry{ - {name: "secured", host: "secured", port: 8080, auth: true}, - {name: "open", host: "open", port: 8081, auth: false}, - }, - persistenceMode: driverSQLite, - } - - data, _ := buildConfig(opts) - - var result map[string]any - if err := json.Unmarshal(data, &result); err != nil { - t.Fatalf("invalid JSON: %v", err) - } - - auth, ok := result["auth"].(map[string]any) - if !ok { - t.Fatal("expected auth section") - } - - scheme, _ := auth["scheme"].(map[string]any) - mappings, ok := scheme["mappings"].([]any) - if !ok || len(mappings) != 1 { - t.Fatalf("expected exactly 1 auth mapping, got %v", scheme["mappings"]) - } - - mapping, _ := mappings[0].(map[string]any) - if mapping["backend"] != "secured" { - t.Errorf("expected auth mapping for 'secured', got %v", mapping["backend"]) - } -} - -func TestBuildConfig_NoAuthWhenNoneEnabled(t *testing.T) { - t.Parallel() - - opts := &configOptions{ - backends: []backendEntry{{name: "open", host: "open", port: 8080, auth: false}}, - persistenceMode: driverSQLite, - } - - data, _ := buildConfig(opts) - - var result map[string]any - if err := json.Unmarshal(data, &result); err != nil { - t.Fatalf("invalid JSON: %v", err) - } - - if _, ok := result["auth"]; ok { - t.Error("expected no auth section when no backend has auth enabled") - } -} - -func TestBuildConfig_PostgresPersistence(t *testing.T) { - t.Parallel() - - opts := &configOptions{ - backends: []backendEntry{{name: "svc", host: "svc", port: 80}}, - persistenceMode: driverPostgres, - } - - data, _ := buildConfig(opts) - - var result map[string]any - if err := json.Unmarshal(data, &result); err != nil { - t.Fatalf("invalid JSON: %v", err) - } - - persistence, ok := result["persistence"].(map[string]any) - if !ok { - t.Fatal("missing persistence section") - } - - if persistence["driver"] != driverPostgres { - t.Errorf("expected driver=postgres, got %v", persistence["driver"]) - } -} - -// ---- resolveScrapeJobs / buildPrometheusYML ---- - -func TestResolveScrapeJobs_BackendsAndConnector(t *testing.T) { - t.Parallel() - - opts := &configOptions{ - backends: []backendEntry{ - {name: "echo", host: "echo", port: 15000}, - {name: "api", host: "api-host", port: 8080}, - }, - includeConnector: true, - obsOpts: obsConfigOptions{ - scrapeTargets: []string{"kerberos", "jaeger", "echo", "api"}, - }, - } - - yml := buildPrometheusYML(resolveScrapeJobs(opts)) - - for _, want := range []string{ - "kerberos:9464", - "jaeger:8888", - "echo:9464", - "api-host:9464", - "connector:9464", - } { - if !strings.Contains(yml, want) { - t.Errorf("expected %q in prometheus.yml, got:\n%s", want, yml) - } - } -} - -func TestResolveScrapeJobs_NoEchoWhenNotRegistered(t *testing.T) { - t.Parallel() - - opts := &configOptions{ - backends: []backendEntry{{name: "api", host: "api", port: 8080}}, - obsOpts: obsConfigOptions{ - scrapeTargets: []string{"kerberos", "echo", "api"}, - }, - } - - yml := buildPrometheusYML(resolveScrapeJobs(opts)) - - if strings.Contains(yml, "echo") { - t.Errorf("echo should not appear when it was not registered as a backend:\n%s", yml) - } - - if !strings.Contains(yml, "api:9464") { - t.Error("expected api:9464") - } -} - -func TestResolveScrapeJobs_ConnectorOnlyWhenIncluded(t *testing.T) { - t.Parallel() - - opts := &configOptions{ - includeConnector: false, - obsOpts: obsConfigOptions{scrapeTargets: []string{"kerberos"}}, - } - - yml := buildPrometheusYML(resolveScrapeJobs(opts)) - - if strings.Contains(yml, "connector") { - t.Errorf("connector should not be scraped when not included:\n%s", yml) - } -} - -// ---- buildGrafanaINI ---- - -func TestBuildGrafanaINI_Postgres(t *testing.T) { - t.Parallel() - - opts := &obsConfigOptions{ - grafanaDB: driverPostgres, - grafanaAnonymous: true, - } - - ini := buildGrafanaINI(opts) - - if !strings.Contains(ini, "type = postgres") { - t.Error("expected type = postgres") - } - - if !strings.Contains(ini, "host = postgres:5432") { - t.Error("expected host = postgres:5432") - } - - if !strings.Contains(ini, "enabled = true") { - t.Error("expected enabled = true in auth.anonymous") - } -} - -func TestBuildGrafanaINI_SQLite_NoAnon(t *testing.T) { - t.Parallel() - - opts := &obsConfigOptions{ - grafanaDB: "sqlite3", - grafanaAnonymous: false, - } - - ini := buildGrafanaINI(opts) - - if !strings.Contains(ini, "type = sqlite3") { - t.Error("expected type = sqlite3") - } - - if strings.Contains(ini, "host =") { - t.Error("sqlite config should not contain host") - } - - if !strings.Contains(ini, "enabled = false") { - t.Error("expected enabled = false in auth.anonymous") - } -} - -// ---- buildGrafanaDatasourcesYML ---- - -func TestBuildGrafanaDatasourcesYML(t *testing.T) { - t.Parallel() - - yml := buildGrafanaDatasourcesYML() - - if !strings.Contains(yml, "prometheus") { - t.Error("expected prometheus datasource") - } - - if !strings.Contains(yml, "http://prometheus:9090") { - t.Error("expected prometheus URL") - } -} - -// ---- buildGrafanaDashboardsYML ---- - -func TestBuildGrafanaDashboardsYML(t *testing.T) { - t.Parallel() - - yml := buildGrafanaDashboardsYML() - - for _, dashboard := range []string{"prometheus.json", "kerberos_runtime.json", "kerberos_http.json"} { - if !strings.Contains(yml, dashboard) { - t.Errorf("expected dashboard %q in grafana-dashboards.yml", dashboard) - } - } -} - -// ---- buildJaegerYML ---- - -func TestBuildJaegerYML(t *testing.T) { - t.Parallel() - - yml := buildJaegerYML() - - if !strings.Contains(yml, "otlp") { - t.Error("expected otlp receiver in jaeger.yml") - } - - if !strings.Contains(yml, "badger_store") { - t.Error("expected badger_store backend") - } - - if !strings.Contains(yml, "16686") { - t.Error("expected jaeger query port 16686") - } -} - -// ---- buildCompose ---- - -func TestBuildCompose_NoEnvVarInjection(t *testing.T) { - t.Parallel() - - opts := &composeOptions{ - includeEcho: true, - includeObsStack: true, - includePostgres: false, - includeConnector: true, - } - - out := buildCompose(opts) - - if strings.Contains(out, "${") { - t.Error("compose output should not contain any ${...} env var injection") - } - - for _, want := range []string{ - "ghcr.io/trebent/kerberos:latest", - "- 30000:30000", - "- 30001:30001", - "- 3000:3000", - "- 16686:16686", - "LOG_VERBOSITY=0", - "krbdata:/data", - } { - if !strings.Contains(out, want) { - t.Errorf("expected compose output to contain %q", want) - } - } - - // Only kerberos gw/admin, grafana and jaeger publish host ports. - for _, notWant := range []string{ - "- 9464:9464", - "- 15000:15000", - "- 30100:30100", - "- 9090:9090", - "LOG_VERBOSITY=20", - } { - if strings.Contains(out, notWant) { - t.Errorf("compose output should not contain %q", notWant) - } - } -} - -func TestBuildCompose_SharedSqliteVolumeOmittedWithPostgres(t *testing.T) { - t.Parallel() - - out := buildCompose(&composeOptions{includeConnector: true, includePostgres: true}) - - if strings.Contains(out, "krbdata") { - t.Errorf("krbdata volume should not be present when Postgres is enabled:\n%s", out) - } -} - -// ---- buildConnectorJSON ---- - -func TestBuildConnectorJSON_SQLite(t *testing.T) { - t.Parallel() - - opts := &connectorOptions{ - allowAllOrigins: true, - persistenceMode: driverSQLite, - } - - data, err := buildConnectorJSON(opts) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - var result map[string]any - if err := json.Unmarshal(data, &result); err != nil { - t.Fatalf("invalid JSON: %v", err) - } - - origins, ok := result["origins"].(map[string]any) - if !ok { - t.Fatal("missing origins section") - } - - if origins["allowAll"] != true { - t.Errorf("expected allowAll=true, got %v", origins["allowAll"]) - } - - if _, ok := origins["denyAll"]; ok { - t.Error("did not expect denyAll when allowAll is set") - } - - persistence, ok := result["persistence"].(map[string]any) - if !ok { - t.Fatal("missing persistence section") - } - - if persistence["driver"] != driverSQLite { - t.Errorf("expected driver=sqlite, got %v", persistence["driver"]) - } - - if persistence["address"] != sqliteSharedPath { - t.Errorf("expected shared sqlite address %q, got %v", sqliteSharedPath, persistence["address"]) - } -} - -func TestBuildConnectorJSON_DenyAll(t *testing.T) { - t.Parallel() - - opts := &connectorOptions{ - allowAllOrigins: false, - persistenceMode: driverSQLite, - } - - data, err := buildConnectorJSON(opts) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - var result map[string]any - if err := json.Unmarshal(data, &result); err != nil { - t.Fatalf("invalid JSON: %v", err) - } - - origins, _ := result["origins"].(map[string]any) - if origins["denyAll"] != true { - t.Errorf("expected denyAll=true, got %v", origins["denyAll"]) - } - - if _, ok := origins["allowAll"]; ok { - t.Error("did not expect allowAll when denyAll is set") - } -} diff --git a/cmd/krbctl/cmd/compose.go b/cmd/krbctl/cmd/compose.go index 6946e6b..df6c4da 100644 --- a/cmd/krbctl/cmd/compose.go +++ b/cmd/krbctl/cmd/compose.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "path/filepath" "strings" "github.com/charmbracelet/huh" @@ -12,11 +13,13 @@ import ( // composeOptions holds the answers collected from the interactive compose session. type composeOptions struct { + outputPath string + + // Enablement flags. includeEcho bool includeObsStack bool includePostgres bool includeConnector bool - outputPath string } func newComposeCmd() *cobra.Command { @@ -29,7 +32,7 @@ postgres, admin-connector, echo) can be included or skipped at each step.`, RunE: runCompose, } - cmd.Flags().StringP("output", "o", "compose.yaml", "Path to write the generated compose.yaml") + cmd.Flags().StringP("output", "o", ".", "Output path where compose.yaml will be written.") return cmd } @@ -75,7 +78,9 @@ func runCompose(cmd *cobra.Command, _ []string) error { content := buildCompose(opts) - if err := os.WriteFile(opts.outputPath, []byte(content), 0o600); err != nil { + if err := os.WriteFile( + filepath.Join(opts.outputPath, "compose.yaml"), []byte(content), 0o644, + ); err != nil { return fmt.Errorf("failed to write compose file: %w", err) } @@ -89,6 +94,7 @@ func buildCompose(opts *composeOptions) string { b.WriteString("services:\n") writePostgresService(&b, opts) + writeSQLiteInitService(&b, opts) writeKerberosService(&b, opts) writeEchoService(&b, opts) writeConnectorService(&b, opts) @@ -133,6 +139,11 @@ func writeKerberosService(b *strings.Builder, opts *composeOptions) { b.WriteString(` depends_on: postgres: condition: service_healthy +`) + } else { + b.WriteString(` depends_on: + sqlite-init: + condition: service_completed_successfully `) } @@ -153,10 +164,7 @@ func writeKerberosService(b *strings.Builder, opts *composeOptions) { - ./krb.json:/krb.json:ro `) - if !opts.includePostgres { - b.WriteString(" - krbdata:/data\n") - } - + b.WriteString(fmt.Sprintf(" %s\n", krbDataMount(opts.includePostgres))) b.WriteString("\n") } @@ -189,23 +197,30 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { depends_on: kerberos: condition: service_started - restart: on-failure +`) + if !opts.includePostgres { + b.WriteString(` sqlite-init: + condition: service_completed_successfully +`) + } + b.WriteString(` restart: on-failure environment: - LOG_TO_CONSOLE=true - LOG_VERBOSITY=0 - PORT=30100 `) + if opts.includeObsStack { + fmt.Fprintf(b, " - TARGET=jaeger:16686\n") + } + writeOtelEnv(b, opts.includeObsStack, "connector") b.WriteString(` volumes: - ./connector.json:/connector.json:ro `) - if !opts.includePostgres { - b.WriteString(" - krbdata:/data\n") - } - + b.WriteString(fmt.Sprintf(" %s\n", krbDataMount(opts.includePostgres))) b.WriteString("\n") } @@ -228,7 +243,14 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { image: "grafana/grafana:13.1" pull_policy: if_not_present restart: on-failure - ports: +`) + if !opts.includePostgres { + b.WriteString(` depends_on: + sqlite-init: + condition: service_completed_successfully +`) + } + b.WriteString(` ports: - 3000:3000 volumes: - ./grafana/grafana.ini:/etc/grafana/grafana.ini @@ -238,8 +260,9 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { - ./grafana/kerberos_runtime.json:/var/lib/grafana/kerberos_runtime.json - ./grafana/kerberos_http.json:/var/lib/grafana/kerberos_http.json - grafana:/var/lib/grafana - - jaeger-init: +`) + b.WriteString(fmt.Sprintf(" %s\n\n", krbDataMount(opts.includePostgres))) + b.WriteString(` jaeger-init: image: busybox:1.38 pull_policy: if_not_present command: ["sh", "-c", "chown 10001:0 /jaeger"] @@ -255,15 +278,44 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { condition: service_completed_successfully restart: on-failure command: --config /jaeger.yml - ports: +`) + if !opts.includeConnector { + b.WriteString(` ports: - 16686:16686 - volumes: +`) + } + b.WriteString(` volumes: - ./jaeger.yml:/jaeger.yml + - ./jaeger-config-ui.json:/jaeger-config-ui.json - jaeger:/jaeger `) } +func writeSQLiteInitService(b *strings.Builder, opts *composeOptions) { + if opts.includePostgres { + return + } + + b.WriteString(` sqlite-init: + image: busybox:1.38 + pull_policy: if_not_present + command: ["sh", "-c", "chmod 0777 /krbdata && touch /krbdata/krb.db && chmod 0666 /krbdata/krb.db"] + restart: on-failure + volumes: + - krbdata:/krbdata + +`) +} + +func krbDataMount(includePostgres bool) string { + if includePostgres { + return "" + } + + return "- krbdata:/krbdata" +} + func writeOtelEnv(b *strings.Builder, withObs bool, hostname string) { if withObs { fmt.Fprintf(b, " - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317\n") diff --git a/cmd/krbctl/cmd/config.go b/cmd/krbctl/cmd/config.go index eacdd1d..93a99b6 100644 --- a/cmd/krbctl/cmd/config.go +++ b/cmd/krbctl/cmd/config.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strconv" "strings" @@ -27,15 +28,18 @@ const ( scrapeMetricsPort = 9464 // sqliteSharedPath is the SQLite file location on the shared krbdata volume, // letting Kerberos and the admin-connector share one database file. - sqliteSharedPath = "/data/krb.db" + sqliteSharedPath = "/krbdata/krb.db" ) // configOptions holds all answers collected from the interactive config session. type configOptions struct { + outputPath string + // Kerberos gateway - backends []backendEntry - persistenceMode string // "sqlite" or "postgres" - outputPath string + backends []backendEntry + + // persistence driver selection + driver string // "sqlite" or "postgres" // Observability stack (Prometheus/Grafana/Jaeger) config generation includeObsStack bool @@ -56,14 +60,12 @@ type backendEntry struct { // obsConfigOptions holds the answers for the observability stack config section. type obsConfigOptions struct { scrapeTargets []string // e.g. ["kerberos","echo","connector","jaeger"] - grafanaDB string // "postgres" or "sqlite" grafanaAnonymous bool } // connectorOptions holds the answers for the admin-connector config section. type connectorOptions struct { allowAllOrigins bool - persistenceMode string // "sqlite" or "postgres" } func newConfigCmd() *cobra.Command { @@ -78,7 +80,7 @@ skipped.`, RunE: runConfig, } - cmd.Flags().StringP("output", "o", "krb.json", "Path to write the generated krb.json") + cmd.Flags().StringP("output", "o", ".", "Output path where config files will be written.") return cmd } @@ -93,11 +95,9 @@ func runConfig(cmd *cobra.Command, _ []string) error { outputPath: output, connectorOpts: connectorOptions{ allowAllOrigins: true, - persistenceMode: driverSQLite, }, - persistenceMode: driverSQLite, + driver: driverSQLite, obsOpts: obsConfigOptions{ - grafanaDB: driverPostgres, grafanaAnonymous: true, }, } @@ -116,7 +116,9 @@ func runConfig(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to build config: %w", err) } - if err := os.WriteFile(opts.outputPath, content, 0o600); err != nil { + if err := os.WriteFile( + filepath.Join(opts.outputPath, "krb.json"), content, 0o644, + ); err != nil { return fmt.Errorf("failed to write config file: %w", err) } @@ -124,19 +126,21 @@ func runConfig(cmd *cobra.Command, _ []string) error { // Write observability stack config files if opts.includeObsStack { - if err := writeObsFiles(opts); err != nil { + if err := writeObsFiles(opts.driver, opts); err != nil { return err } } // Write connector.json if opts.includeConnector { - connContent, err := buildConnectorJSON(&opts.connectorOpts) + connContent, err := buildConnectorJSON(opts.driver, &opts.connectorOpts) if err != nil { return fmt.Errorf("failed to build connector config: %w", err) } - if err := os.WriteFile("connector.json", connContent, 0o600); err != nil { + if err := os.WriteFile( + filepath.Join(opts.outputPath, "connector.json"), connContent, 0o644, + ); err != nil { return fmt.Errorf("failed to write connector.json: %w", err) } @@ -308,10 +312,10 @@ func promptFixedSections(opts *configOptions) error { opts.obsOpts.scrapeTargets = defaultScrapeTargets(opts) form := huh.NewForm( + buildPersistenceGroup(opts), buildAuthGroup(opts), buildObsToggleGroup(opts), buildObsOptionsGroup(opts), - buildPersistenceGroup(opts), buildConnectorToggleGroup(opts), buildConnectorConfigGroup(opts), ) @@ -364,11 +368,6 @@ func buildObsToggleGroup(opts *configOptions) *huh.Group { // buildObsOptionsGroup builds the observability stack detail options, hidden // unless the observability stack toggle is enabled. func buildObsOptionsGroup(opts *configOptions) *huh.Group { - grafanaDBOpts := []huh.Option[string]{ - huh.NewOption("PostgreSQL", driverPostgres), - huh.NewOption("SQLite (Grafana default)", "sqlite3"), - } - scrapeOpts := []huh.Option[string]{ huh.NewOption(fmt.Sprintf("kerberos (port %d)", scrapeMetricsPort), defaultKRBDB), huh.NewOption("jaeger (port 8888)", jaegerName), @@ -386,11 +385,6 @@ func buildObsOptionsGroup(opts *configOptions) *huh.Group { Options(scrapeOpts...). Value(&opts.obsOpts.scrapeTargets), - huh.NewSelect[string](). - Title("Grafana database backend"). - Options(grafanaDBOpts...). - Value(&opts.obsOpts.grafanaDB), - huh.NewConfirm(). Title("Enable Grafana anonymous access?"). Description("Allows viewing dashboards without logging in."). @@ -411,7 +405,7 @@ func buildPersistenceGroup(opts *configOptions) *huh.Group { huh.NewSelect[string](). Title("Persistence backend"). Options(persistenceOpts...). - Value(&opts.persistenceMode), + Value(&opts.driver), ).Title("Persistence") } @@ -429,22 +423,12 @@ func buildConnectorToggleGroup(opts *configOptions) *huh.Group { // buildConnectorConfigGroup builds the admin-connector config detail section, // hidden unless the admin-connector toggle is enabled. func buildConnectorConfigGroup(opts *configOptions) *huh.Group { - persistenceOpts := []huh.Option[string]{ - huh.NewOption("SQLite (default, file-based)", driverSQLite), - huh.NewOption("PostgreSQL", driverPostgres), - } - return huh.NewGroup( huh.NewConfirm(). Title("Allow all CORS origins?"). Description("Yes allows any origin; No denies all cross-origin requests."). WithButtonAlignment(lipgloss.Left). Value(&opts.connectorOpts.allowAllOrigins), - - huh.NewSelect[string](). - Title("Connector persistence backend"). - Options(persistenceOpts...). - Value(&opts.connectorOpts.persistenceMode), ).Title("Admin-connector config"). WithHideFunc(func() bool { return !opts.includeConnector }) } @@ -488,7 +472,7 @@ func buildConfig(opts *configOptions) ([]byte, error) { root["auth"] = buildAuthSection(authBackends) } - root["persistence"] = buildPersistenceSection(opts.persistenceMode) + root["persistence"] = buildPersistenceSection(opts.driver) return json.MarshalIndent(root, "", " ") } diff --git a/cmd/krbctl/cmd/connectorconfig.go b/cmd/krbctl/cmd/connectorconfig.go index 1d95904..c1f972e 100644 --- a/cmd/krbctl/cmd/connectorconfig.go +++ b/cmd/krbctl/cmd/connectorconfig.go @@ -6,7 +6,7 @@ import ( ) // buildConnectorJSON generates a minimal connector.json. -func buildConnectorJSON(opts *connectorOptions) ([]byte, error) { +func buildConnectorJSON(driver string, opts *connectorOptions) ([]byte, error) { origins := map[string]any{} if opts.allowAllOrigins { origins["allowAll"] = true @@ -16,7 +16,7 @@ func buildConnectorJSON(opts *connectorOptions) ([]byte, error) { root := map[string]any{ "origins": origins, - "persistence": buildPersistenceSection(opts.persistenceMode), + "persistence": buildPersistenceSection(driver), } content, err := json.MarshalIndent(root, "", " ") diff --git a/cmd/krbctl/cmd/obsconfig.go b/cmd/krbctl/cmd/obsconfig.go index 9d0431a..6f10455 100644 --- a/cmd/krbctl/cmd/obsconfig.go +++ b/cmd/krbctl/cmd/obsconfig.go @@ -4,6 +4,7 @@ import ( _ "embed" "fmt" "os" + "path/filepath" "strings" ) @@ -16,6 +17,9 @@ var grafanaDashboardRuntime []byte //go:embed assets/grafana/kerberos_http.json var grafanaDashboardHTTP []byte +//go:embed assets/jaeger/config-ui.json +var jaegerConfigUI []byte + // scrapeJob is a resolved Prometheus scrape target (job name + host:port). type scrapeJob struct { name string @@ -23,13 +27,16 @@ type scrapeJob struct { } // writeObsFiles creates all observability config files in the current directory. -func writeObsFiles(opts *configOptions) error { +func writeObsFiles(driver string, opts *configOptions) error { prometheusData := []byte(buildPrometheusYML(resolveScrapeJobs(opts))) - if err := writeObsFile("prometheus.yml", prometheusData); err != nil { + if err := writeObsFile( + filepath.Join(opts.outputPath, "prometheus.yml"), prometheusData, + ); err != nil { return err } - if err := os.MkdirAll("grafana", 0o750); err != nil { + grafanaDir := filepath.Join(opts.outputPath, "grafana") + if err := os.MkdirAll(grafanaDir, 0o750); err != nil { return fmt.Errorf("failed to create grafana directory: %w", err) } @@ -37,12 +44,18 @@ func writeObsFiles(opts *configOptions) error { path string data []byte }{ - {"grafana/grafana.ini", []byte(buildGrafanaINI(&opts.obsOpts))}, - {"grafana/grafana-datasources.yml", []byte(buildGrafanaDatasourcesYML())}, - {"grafana/grafana-dashboards.yml", []byte(buildGrafanaDashboardsYML())}, - {"grafana/prometheus.json", grafanaDashboardPrometheus}, - {"grafana/kerberos_runtime.json", grafanaDashboardRuntime}, - {"grafana/kerberos_http.json", grafanaDashboardHTTP}, + {filepath.Join(grafanaDir, "grafana.ini"), []byte(buildGrafanaINI(driver, &opts.obsOpts))}, + { + filepath.Join(grafanaDir, "grafana-datasources.yml"), + []byte(buildGrafanaDatasourcesYML()), + }, + { + filepath.Join(grafanaDir, "grafana-dashboards.yml"), + []byte(buildGrafanaDashboardsYML()), + }, + {filepath.Join(grafanaDir, "prometheus.json"), grafanaDashboardPrometheus}, + {filepath.Join(grafanaDir, "kerberos_runtime.json"), grafanaDashboardRuntime}, + {filepath.Join(grafanaDir, "kerberos_http.json"), grafanaDashboardHTTP}, } for _, f := range grafanaFiles { @@ -51,12 +64,24 @@ func writeObsFiles(opts *configOptions) error { } } - return writeObsFile("jaeger.yml", []byte(buildJaegerYML())) + if err := writeObsFile( + filepath.Join(opts.outputPath, "jaeger.yml"), []byte(buildJaegerYML()), + ); err != nil { + return err + } + + if err := writeObsFile( + filepath.Join(opts.outputPath, "jaeger-config-ui.json"), jaegerConfigUI, + ); err != nil { + return err + } + + return nil } // writeObsFile writes data to path and prints a confirmation line. func writeObsFile(path string, data []byte) error { - if err := os.WriteFile(path, data, 0o600); err != nil { + if err := os.WriteFile(path, data, 0o644); err != nil { return fmt.Errorf("failed to write %s: %w", path, err) } @@ -121,12 +146,12 @@ func buildPrometheusYML(jobs []scrapeJob) string { } // buildGrafanaINI generates a slim grafana.ini with only the sections used in this deployment. -func buildGrafanaINI(opts *obsConfigOptions) string { +func buildGrafanaINI(driver string, opts *obsConfigOptions) string { var b strings.Builder b.WriteString("[database]\n") - if opts.grafanaDB == driverPostgres { + if driver == driverPostgres { b.WriteString("type = postgres\n") b.WriteString("host = postgres:5432\n") b.WriteString("name = kerberos\n") @@ -134,6 +159,7 @@ func buildGrafanaINI(opts *obsConfigOptions) string { b.WriteString("password = kerberos\n") } else { b.WriteString("type = sqlite3\n") + b.WriteString(fmt.Sprintf("path = %s\n", sqliteSharedPath)) } b.WriteString("\n[auth.anonymous]\n") @@ -210,7 +236,7 @@ extensions: traces: badger_store traces_archive: badger_archive ui: - config_file: /config-ui.json + config_file: /jaeger-config-ui.json http: endpoint: 0.0.0.0:16686 grpc: From 230a4a55dd215661e1f32270451320e4e5168a77 Mon Sep 17 00:00:00 2001 From: maansaake Date: Sat, 22 Aug 2026 12:52:51 +0200 Subject: [PATCH 09/13] make connector print its target --- cmd/admin-connector/main.go | 2 +- cmd/krbctl/cmd/compose.go | 9 ++++++--- cmd/krbctl/cmd/obsconfig.go | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/admin-connector/main.go b/cmd/admin-connector/main.go index e891ef1..90ce086 100644 --- a/cmd/admin-connector/main.go +++ b/cmd/admin-connector/main.go @@ -66,7 +66,7 @@ func main() { WithName("admin-connector"), ) - zerologr.Info("Starting admin connector", "port", port.Value()) + zerologr.Info("Starting admin connector", "port", port.Value(), "target", target.Value()) signalCtx, signalCancel := signal.NotifyContext( context.Background(), diff --git a/cmd/krbctl/cmd/compose.go b/cmd/krbctl/cmd/compose.go index df6c4da..0c386b0 100644 --- a/cmd/krbctl/cmd/compose.go +++ b/cmd/krbctl/cmd/compose.go @@ -164,7 +164,7 @@ func writeKerberosService(b *strings.Builder, opts *composeOptions) { - ./krb.json:/krb.json:ro `) - b.WriteString(fmt.Sprintf(" %s\n", krbDataMount(opts.includePostgres))) + fmt.Fprintf(b, " %s\n", krbDataMount(opts.includePostgres)) b.WriteString("\n") } @@ -203,6 +203,9 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { condition: service_completed_successfully `) } + b.WriteString(` ports: + - 30100:30100 +`) b.WriteString(` restart: on-failure environment: - LOG_TO_CONSOLE=true @@ -220,7 +223,7 @@ func writeConnectorService(b *strings.Builder, opts *composeOptions) { - ./connector.json:/connector.json:ro `) - b.WriteString(fmt.Sprintf(" %s\n", krbDataMount(opts.includePostgres))) + fmt.Fprintf(b, " %s\n", krbDataMount(opts.includePostgres)) b.WriteString("\n") } @@ -261,7 +264,7 @@ func writeObsServices(b *strings.Builder, opts *composeOptions) { - ./grafana/kerberos_http.json:/var/lib/grafana/kerberos_http.json - grafana:/var/lib/grafana `) - b.WriteString(fmt.Sprintf(" %s\n\n", krbDataMount(opts.includePostgres))) + fmt.Fprintf(b, " %s\n\n", krbDataMount(opts.includePostgres)) b.WriteString(` jaeger-init: image: busybox:1.38 pull_policy: if_not_present diff --git a/cmd/krbctl/cmd/obsconfig.go b/cmd/krbctl/cmd/obsconfig.go index 6f10455..ce68f55 100644 --- a/cmd/krbctl/cmd/obsconfig.go +++ b/cmd/krbctl/cmd/obsconfig.go @@ -159,7 +159,7 @@ func buildGrafanaINI(driver string, opts *obsConfigOptions) string { b.WriteString("password = kerberos\n") } else { b.WriteString("type = sqlite3\n") - b.WriteString(fmt.Sprintf("path = %s\n", sqliteSharedPath)) + fmt.Fprintf(&b, "path = %s\n", sqliteSharedPath) } b.WriteString("\n[auth.anonymous]\n") From c813a9f7c11cc8d719d91ac142bfddd839db5a13 Mon Sep 17 00:00:00 2001 From: maansaake Date: Sat, 22 Aug 2026 12:54:59 +0200 Subject: [PATCH 10/13] go mod tidy --- go.mod | 6 +++--- go.sum | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 492622e..5b25085 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,15 @@ module github.com/trebent/kerberos go 1.26.5 require ( + github.com/charmbracelet/huh v1.0.0 + github.com/charmbracelet/lipgloss v1.1.0 github.com/getkin/kin-openapi v0.146.0 github.com/go-logr/logr v1.4.4 github.com/google/uuid v1.6.0 github.com/lib/pq v1.12.3 github.com/oapi-codegen/nethttp-middleware v1.2.0 - github.com/spf13/cobra v1.9.1 github.com/oapi-codegen/runtime v1.7.0 + github.com/spf13/cobra v1.9.1 github.com/trebent/envparser v1.0.8 github.com/trebent/zerologr v1.1.1 github.com/xeipuuv/gojsonschema v1.2.0 @@ -36,8 +38,6 @@ require ( github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect github.com/charmbracelet/bubbletea v1.3.6 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/huh v1.0.0 // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect diff --git a/go.sum b/go.sum index 09abc77..eb012ab 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= @@ -5,6 +7,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -28,11 +32,23 @@ github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -215,6 +231,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= From 251adc3eef1e40f360ef059f9baf190d0ec95aca Mon Sep 17 00:00:00 2001 From: maansaake Date: Sat, 22 Aug 2026 13:10:39 +0200 Subject: [PATCH 11/13] resolve golint, bump go.mod --- cmd/krbctl/cmd/compose.go | 1 + cmd/krbctl/cmd/config.go | 2 ++ cmd/krbctl/cmd/obsconfig.go | 1 + go.mod | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/krbctl/cmd/compose.go b/cmd/krbctl/cmd/compose.go index 0c386b0..78387ef 100644 --- a/cmd/krbctl/cmd/compose.go +++ b/cmd/krbctl/cmd/compose.go @@ -78,6 +78,7 @@ func runCompose(cmd *cobra.Command, _ []string) error { content := buildCompose(opts) + //nolint:gosec // welp if err := os.WriteFile( filepath.Join(opts.outputPath, "compose.yaml"), []byte(content), 0o644, ); err != nil { diff --git a/cmd/krbctl/cmd/config.go b/cmd/krbctl/cmd/config.go index 93a99b6..0e4005b 100644 --- a/cmd/krbctl/cmd/config.go +++ b/cmd/krbctl/cmd/config.go @@ -116,6 +116,7 @@ func runConfig(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to build config: %w", err) } + //nolint:gosec // welp if err := os.WriteFile( filepath.Join(opts.outputPath, "krb.json"), content, 0o644, ); err != nil { @@ -138,6 +139,7 @@ func runConfig(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to build connector config: %w", err) } + //nolint:gosec // welp if err := os.WriteFile( filepath.Join(opts.outputPath, "connector.json"), connContent, 0o644, ); err != nil { diff --git a/cmd/krbctl/cmd/obsconfig.go b/cmd/krbctl/cmd/obsconfig.go index ce68f55..f079050 100644 --- a/cmd/krbctl/cmd/obsconfig.go +++ b/cmd/krbctl/cmd/obsconfig.go @@ -81,6 +81,7 @@ func writeObsFiles(driver string, opts *configOptions) error { // writeObsFile writes data to path and prints a confirmation line. func writeObsFile(path string, data []byte) error { + //nolint:gosec // welp if err := os.WriteFile(path, data, 0o644); err != nil { return fmt.Errorf("failed to write %s: %w", path, err) } diff --git a/go.mod b/go.mod index 5b25085..e69791a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/trebent/kerberos -go 1.26.5 +go 1.26.6 require ( github.com/charmbracelet/huh v1.0.0 From 10cb2194c642916733de05c9871ed1c72b8fc5ea Mon Sep 17 00:00:00 2001 From: maansaake Date: Sat, 22 Aug 2026 13:18:39 +0200 Subject: [PATCH 12/13] remove callable-cli, fold into build --- .github/workflows/callable-build.yaml | 137 +++++++++++++++++- .github/workflows/callable-cli.yaml | 102 ------------- .../{release-cli.yaml => release-krbctl.yaml} | 6 +- Makefile | 5 +- 4 files changed, 140 insertions(+), 110 deletions(-) delete mode 100644 .github/workflows/callable-cli.yaml rename .github/workflows/{release-cli.yaml => release-krbctl.yaml} (91%) diff --git a/.github/workflows/callable-build.yaml b/.github/workflows/callable-build.yaml index 16346f5..ee3ce6d 100644 --- a/.github/workflows/callable-build.yaml +++ b/.github/workflows/callable-build.yaml @@ -60,7 +60,7 @@ jobs: exit 1 fi - go-build: + kerberos: runs-on: ubuntu-latest steps: - name: Checkout @@ -104,3 +104,138 @@ jobs: - name: Build run: make build + + echo: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Golang + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - name: Compute build cache key + id: build-cache-key + run: | + checksum=$( + { + cat go.mod + [ -f go.sum ] && cat go.sum + cat Makefile + find . -type f -name '*.go' \ + -not -path './test/suites/*' \ + -not -path './tools/*' \ + -print0 | sort -z | xargs -0 cat + } | md5sum | awk '{print $1}' + ) + echo "checksum=$checksum" >> "$GITHUB_OUTPUT" + + - name: Cache Go module cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/go/pkg/mod + key: echo-go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} + + - name: Cache Go build cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/go-build + key: echo-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} + restore-keys: | + echo-go-build-${{ runner.os }}- + + - name: Build + run: make echo/build + + connector: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Golang + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - name: Compute build cache key + id: build-cache-key + run: | + checksum=$( + { + cat go.mod + [ -f go.sum ] && cat go.sum + cat Makefile + find . -type f -name '*.go' \ + -not -path './test/suites/*' \ + -not -path './tools/*' \ + -print0 | sort -z | xargs -0 cat + } | md5sum | awk '{print $1}' + ) + echo "checksum=$checksum" >> "$GITHUB_OUTPUT" + + - name: Cache Go module cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/go/pkg/mod + key: connector-go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} + + - name: Cache Go build cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/go-build + key: connector-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} + restore-keys: | + connector-go-build-${{ runner.os }}- + + - name: Build + run: make connector/build + + krbctl: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Golang + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - name: Compute build cache key + id: build-cache-key + run: | + checksum=$( + { + cat go.mod + [ -f go.sum ] && cat go.sum + cat Makefile + find . -type f -name '*.go' \ + -not -path './test/suites/*' \ + -not -path './tools/*' \ + -print0 | sort -z | xargs -0 cat + } | md5sum | awk '{print $1}' + ) + echo "checksum=$checksum" >> "$GITHUB_OUTPUT" + + - name: Cache Go module cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/go/pkg/mod + key: krbctl-go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} + + - name: Cache Go build cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/go-build + key: krbctl-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} + restore-keys: | + krbctl-go-build-${{ runner.os }}- + + - name: Build + run: make krbctl/build diff --git a/.github/workflows/callable-cli.yaml b/.github/workflows/callable-cli.yaml deleted file mode 100644 index f2e4ae7..0000000 --- a/.github/workflows/callable-cli.yaml +++ /dev/null @@ -1,102 +0,0 @@ -name: krbctl CLI - -on: - workflow_call: - -concurrency: - group: cli-${{ github.event.pull_request.number || github.ref_name }} - cancel-in-progress: false - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Set up Golang - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - cache: false - - - name: Compute build cache key - id: build-cache-key - run: | - checksum=$( - { - cat go.mod - [ -f go.sum ] && cat go.sum - cat Makefile - find . -type f -name '*.go' \ - -not -path './test/suites/*' \ - -not -path './tools/*' \ - -print0 | sort -z | xargs -0 cat - } | md5sum | awk '{print $1}' - ) - echo "checksum=$checksum" >> "$GITHUB_OUTPUT" - - - name: Cache Go module cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/go/pkg/mod - key: cli-build-go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} - - - name: Cache Go build cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cache/go-build - key: cli-build-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} - restore-keys: | - cli-build-go-build-${{ runner.os }}- - - - name: Build krbctl - run: make krbctl/build - - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Set up Golang - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - cache: false - - - name: Compute build cache key - id: build-cache-key - run: | - checksum=$( - { - cat go.mod - [ -f go.sum ] && cat go.sum - cat Makefile - find . -type f -name '*.go' \ - -not -path './test/suites/*' \ - -not -path './tools/*' \ - -print0 | sort -z | xargs -0 cat - } | md5sum | awk '{print $1}' - ) - echo "checksum=$checksum" >> "$GITHUB_OUTPUT" - - - name: Cache Go module cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/go/pkg/mod - key: cli-test-go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} - - - name: Cache Go build cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cache/go-build - key: cli-test-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} - restore-keys: | - cli-test-go-build-${{ runner.os }}- - - - name: Test krbctl - run: make krbctl/test diff --git a/.github/workflows/release-cli.yaml b/.github/workflows/release-krbctl.yaml similarity index 91% rename from .github/workflows/release-cli.yaml rename to .github/workflows/release-krbctl.yaml index cc5cf70..4736e6d 100644 --- a/.github/workflows/release-cli.yaml +++ b/.github/workflows/release-krbctl.yaml @@ -1,4 +1,4 @@ -name: Release krbctl CLI +name: Release krbctl on: release: @@ -53,9 +53,9 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/go-build - key: cli-release-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} + key: krbctl-release-go-build-${{ runner.os }}-${{ steps.build-cache-key.outputs.checksum }} restore-keys: | - cli-release-go-build-${{ runner.os }}- + krbctl-release-go-build-${{ runner.os }}- - name: Build release artifacts run: make krbctl/release VERSION=${{ github.ref_name }} diff --git a/Makefile b/Makefile index c61b843..660eb49 100644 --- a/Makefile +++ b/Makefile @@ -228,6 +228,7 @@ docker/network/rm: echo/build: $(call cecho,Building Echo binary...,$(BOLD_YELLOW)) + @mkdir -p build @CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o build/echo ./cmd/echo echo/docker/build: @@ -333,10 +334,6 @@ krbctl/install: $(call cecho,Installing krbctl binary to $(GOBIN)...,$(BOLD_YELLOW)) @CGO_ENABLED=0 GOOS=linux go install -trimpath -ldflags="$(KRBCTL_LDFLAGS)" ./cmd/krbctl -krbctl/test: - $(call cecho,Running krbctl tests...,$(BOLD_YELLOW)) - @go test -v ./cmd/krbctl/... -failfast - krbctl/release: $(call cecho,Building krbctl release artifacts for version $(VERSION)...,$(BOLD_YELLOW)) @rm -rf build/release From b9cefe238a37cbcb8cc3d4899f26963ff34f3d2d Mon Sep 17 00:00:00 2001 From: maansaake Date: Sat, 22 Aug 2026 13:19:22 +0200 Subject: [PATCH 13/13] remove calls to cli --- .github/workflows/main.yaml | 4 ---- .github/workflows/pull-request.yaml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index dc2e62b..e8476dc 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -20,10 +20,6 @@ jobs: uses: ./.github/workflows/callable-build.yaml secrets: inherit - cli: - uses: ./.github/workflows/callable-cli.yaml - secrets: inherit - image: uses: ./.github/workflows/callable-image.yaml secrets: inherit diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index b083bf9..0d2bf9c 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -27,7 +27,3 @@ jobs: unit-test: uses: ./.github/workflows/callable-test-unit.yaml secrets: inherit - - cli: - uses: ./.github/workflows/callable-cli.yaml - secrets: inherit