diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..a542a03 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 + +--- +coverage: + range: 50..80 + round: down + precision: 2 + +ignore: + - "*_test.go" + - "vendor" + +fixes: + - "github.com/xmidt-org/__PROJECT__/::" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8764f8b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 +--- +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every day + interval: "daily" + labels: + - "dependencies" + commit-message: + prefix: "chore" + include: "scope" + open-pull-requests-limit: 10 + + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + labels: + - "dependencies" + commit-message: + prefix: "chore" + include: "scope" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8525fc4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 +--- +name: CI + +on: + push: + branches: + - main + paths-ignore: + - README.md + - CONTRIBUTING.md + - MAINTAINERS.md + - LICENSE + - NOTICE + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + pull_request: + workflow_dispatch: + +jobs: + ci: + uses: xmidt-org/shared-go/.github/workflows/ci.yml@59f5d322b0ee953245334530336f8e6503cacb65 # v4.4.27 + with: + release-type: library + yaml-lint-skip: false + secrets: inherit diff --git a/.github/workflows/dependabot-approver.yml b/.github/workflows/dependabot-approver.yml new file mode 100644 index 0000000..76dcc3c --- /dev/null +++ b/.github/workflows/dependabot-approver.yml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 + +--- +name: 'Dependabot auto approval' + +on: + pull_request_target +permissions: + pull-requests: write + contents: write + +jobs: + package: + uses: xmidt-org/.github/.github/workflows/dependabot-approver-template.yml@main + secrets: inherit diff --git a/.github/workflows/proj-xmidt-team.yml b/.github/workflows/proj-xmidt-team.yml new file mode 100644 index 0000000..8ed5fa9 --- /dev/null +++ b/.github/workflows/proj-xmidt-team.yml @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 + +--- +name: 'PROJ: xmidt-team' + +on: + issues: + types: + - opened + pull_request: + types: + - opened + +jobs: + package: + uses: xmidt-org/.github/.github/workflows/proj-template.yml@proj-v1 + secrets: inherit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbe429e --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +coverage* +report.json + +# Ignore the vendor structure +*vendor/ + +# Ignore the various build artifacts +.ignore + +# Vim +*.swp + +cmd/skeleton/skeleton \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..271969f --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 + +--- +linters: + enable: + - bodyclose + - dupl + - errorlint + - funlen + - goconst + - gosec + - misspell + - unconvert + - prealloc + disable: + - errcheck + - ineffassign + +issues: + exclude-rules: + - path: _test.go + linters: + - dupl + - funlen + +linters-settings: + errorlint: + # Report non-wrapping error creation using fmt.Errorf + errorf: false + misspell: + locale: US diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 0000000..2b5090d --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,44 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: skeleton +Upstream-Contact: Weston Schmidt GET /api/ok HTTP/1.1 +> Host: localhost:10443 +> User-Agent: curl/8.0.1 +> Accept: */* +> +< HTTP/1.1 200 OK +< Date: Wed, 11 Dec 2024 02:00:35 GMT +< Content-Length: 0 +< +* Connection #0 to host localhost left intact +``` \ No newline at end of file diff --git a/cmd/skeleton/main.go b/cmd/skeleton/main.go new file mode 100644 index 0000000..e7d6aca --- /dev/null +++ b/cmd/skeleton/main.go @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "os" + "runtime/debug" + + "github.com/xmidt-org/skeleton" +) + +func main() { + defer func() { + if r := recover(); r != nil { + fmt.Println("stacktrace from panic: \n" + string(debug.Stack())) + } + }() + + err := skeleton.Main(os.Args[1:], true) + + if err == nil { + return + } + + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) +} diff --git a/cmd/skeleton/skeleton.yml b/cmd/skeleton/skeleton.yml new file mode 100644 index 0000000..a8ecf34 --- /dev/null +++ b/cmd/skeleton/skeleton.yml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 +--- +auth: + disable: true +servers: + health: + http: + address: :10080 + path: / + metrics: + http: + address: 127.0.0.1:9361 + path: /metrics + pprof: + http: + address: 127.0.0.1:9999 + path: /debug/pprof + primary: + http: + address: :10443 +oker: + name: "" diff --git a/config.go b/config.go new file mode 100644 index 0000000..b23bd9d --- /dev/null +++ b/config.go @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package skeleton + +import ( + "fmt" + "os" + "time" + + "github.com/goschtalt/goschtalt" + "github.com/xmidt-org/arrange/arrangehttp" + "github.com/xmidt-org/arrange/arrangepprof" + "github.com/xmidt-org/candlelight" + "github.com/xmidt-org/sallust" + "github.com/xmidt-org/skeleton/internal/apiauth" + "github.com/xmidt-org/skeleton/internal/oker" + "github.com/xmidt-org/touchstone" + "github.com/xmidt-org/touchstone/touchhttp" + "gopkg.in/dealancer/validate.v2" +) + +// Config is the top level configuration for the notus service. Everything +// is contained in this structure or it will intentially cause a failure. +type Config struct { + Logging sallust.Config + Tracing candlelight.Config + Prometheus touchstone.Config + PrometheusHandler touchhttp.Config + Servers Servers + Routes Routes + Auth apiauth.Config + Oker oker.Config +} + +type Servers struct { + Health HealthServer + Metrics MetricsServer + Pprof PprofServer + Primary PrimaryServer + Alternate PrimaryServer +} + +type HealthServer struct { + HTTP arrangehttp.ServerConfig + Path HealthPath //`validate:"empty=false"` +} + +type HealthPath string + +type MetricsServer struct { + HTTP arrangehttp.ServerConfig + Path MetricsPath //`validate:"empty=false"` +} + +type MetricsPath string + +type PrimaryServer struct { + HTTP arrangehttp.ServerConfig +} + +type PprofServer struct { + HTTP arrangehttp.ServerConfig + Path PprofPathPrefix +} + +type PprofPathPrefix string + +type Routes struct { + Oker Route +} + +type Route struct { + Path string `validate:"empty=false"` + Server string `validate:"one_of=primary,alternate"` +} + +// Collect and process the configuration files and env vars and +// produce a configuration object. +func provideConfig(cli *CLI) (*goschtalt.Config, error) { + gs, err := goschtalt.New( + goschtalt.StdCfgLayout(applicationName, cli.Files...), + goschtalt.ConfigIs("two_words"), + goschtalt.DefaultUnmarshalOptions( + goschtalt.WithValidator( + goschtalt.ValidatorFunc(validate.Validate), + ), + ), + + // Seed the program with the default, built-in configuration. + // Mark this as a default so it is ordered correctly. + goschtalt.AddValue("built-in", goschtalt.Root, defaultConfig, + goschtalt.AsDefault()), + ) + if err != nil { + return nil, err + } + + if cli.Show { + // handleCLIShow handles the -s/--show option where the configuration is + // shown, then the program is exited. + // + // Exit with success because if the configuration is broken it will be + // very hard to debug where the problem originates. This way you can + // see the configuration and then run the service with the same + // configuration to see the error. + + fmt.Fprintln(os.Stdout, gs.Explain().String()) + + out, err := gs.Marshal() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } else { + fmt.Fprintln(os.Stdout, "## Final Configuration\n---\n"+string(out)) + } + + os.Exit(0) + } + + var tmp Config + err = gs.Unmarshal(goschtalt.Root, &tmp) + if err != nil { + fmt.Fprintln(os.Stderr, "There is a critical error in the configuration.") + fmt.Fprintln(os.Stderr, "Run with -s/--show to see the configuration.") + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + + // Exit here to prevent a very difficult to debug error from occurring. + os.Exit(-1) + } + + return gs, nil +} + +// ----------------------------------------------------------------------------- +// Keep the default configuration at the bottom of the file so it is easy to +// see what the default configuration is. +// ----------------------------------------------------------------------------- + +var defaultConfig = Config{ + Servers: Servers{ + Health: HealthServer{ + HTTP: arrangehttp.ServerConfig{ + Network: "tcp", + Address: ":80", + }, + Path: HealthPath("/"), + }, + Metrics: MetricsServer{ + HTTP: arrangehttp.ServerConfig{ + Network: "tcp", + Address: "127.0.0.1:9361", + }, + Path: MetricsPath("/metrics"), + }, + Pprof: PprofServer{ + HTTP: arrangehttp.ServerConfig{ + Network: "tcp", + Address: "127.0.0.1:9999", + }, + Path: arrangepprof.DefaultPathPrefix, + }, + Primary: PrimaryServer{ + HTTP: arrangehttp.ServerConfig{ + Network: "tcp", + Address: ":443", + }, + }, + Alternate: PrimaryServer{ + HTTP: arrangehttp.ServerConfig{ + Network: "tcp", + Address: "127.0.0.1:8443", + }, + }, + }, + Routes: Routes{ + Oker: Route{ + Path: "/api/ok", + Server: "primary", + }, + }, + Prometheus: touchstone.Config{ + DefaultNamespace: applicationNamespace, + DefaultSubsystem: applicationName, + }, + PrometheusHandler: touchhttp.Config{ + InstrumentMetricHandler: true, + MaxRequestsInFlight: 5, + Timeout: 5 * time.Second, + }, + + Tracing: candlelight.Config{ + ApplicationName: applicationName, + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..98fc42c --- /dev/null +++ b/go.mod @@ -0,0 +1,82 @@ +module github.com/xmidt-org/skeleton + +go 1.23.1 + +require ( + github.com/alecthomas/kong v1.6.0 + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-kit/kit v0.13.0 + github.com/goschtalt/goschtalt v0.25.0 + github.com/goschtalt/yaml-decoder v0.0.1 + github.com/goschtalt/yaml-encoder v0.0.3 + github.com/lestrrat-go/jwx/v2 v2.1.2 + github.com/prometheus/client_golang v1.20.5 + github.com/stretchr/testify v1.10.0 + github.com/xmidt-org/arrange v0.5.1 + github.com/xmidt-org/bascule v1.1.0 + github.com/xmidt-org/candlelight v0.1.22 + github.com/xmidt-org/eventor v1.0.23 + github.com/xmidt-org/httpaux v0.4.1 + github.com/xmidt-org/sallust v0.2.3 + github.com/xmidt-org/touchstone v0.1.7 + go.uber.org/fx v1.23.0 + go.uber.org/zap v1.27.0 + gopkg.in/dealancer/validate.v2 v2.1.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/goschtalt/approx v1.0.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.59.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xmidt-org/wrp-go/v3 v3.6.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b9357d1 --- /dev/null +++ b/go.sum @@ -0,0 +1,187 @@ +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE= +github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/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= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goschtalt/approx v1.0.0 h1:q8DMVEOSgwjFUYsupwhLApMWhfbaxRfWeSKT2uTU214= +github.com/goschtalt/approx v1.0.0/go.mod h1:Mh0VbpeEgO2Qo2PKGrSuz241D/nj9q7OPegJNWzrbIU= +github.com/goschtalt/goschtalt v0.25.0 h1:xf/VhUr8ieHbRG6uf2J4LGqTjdThVTWRJT/4OSCAxiU= +github.com/goschtalt/goschtalt v0.25.0/go.mod h1:VeN+P4rFr9WbdB6uBJR+N5WBU7aFLU+fzoEMiO/6/C4= +github.com/goschtalt/yaml-decoder v0.0.1 h1:fwXf5OoC2tUm6+FOzMizvh6UITFALw6OWxJTVNgbJwg= +github.com/goschtalt/yaml-decoder v0.0.1/go.mod h1:b+hYjmM/e9rzRhPB8EKlb+LUwrgntMrOpqEAel3wRGQ= +github.com/goschtalt/yaml-encoder v0.0.3 h1:vfQ3vXZNvoEFPa3NzOWNtweYVa+2qMh8eqhXByLi2t0= +github.com/goschtalt/yaml-encoder v0.0.3/go.mod h1:E9ANM2mgRmoqP+JTFFv03fVWcnn+QrIDfVu5shDvX3A= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc= +github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef h1:NKxTG6GVGbfMXc2mIk+KphcH6hagbVXhcFkbTgYleTI= +github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xmidt-org/arrange v0.5.1 h1:JHoU8C03ZQfZAG9Vpudj7RQj97pAHblwm6Co8mepU9A= +github.com/xmidt-org/arrange v0.5.1/go.mod h1:zeJWY01z21ihPiAnGNF+bELbJANWwsahGE2bzGyyPIs= +github.com/xmidt-org/bascule v1.1.0 h1:IzdpDQ6HIKaVkQNvbLdmfv7BAmk15SnAJUPyEAVg/Ug= +github.com/xmidt-org/bascule v1.1.0/go.mod h1:9pJ2rCAVNSANd3C276SuJEFwXIjZ8xofHV7LYh2ylWs= +github.com/xmidt-org/candlelight v0.1.22 h1:4cTvBq71s2HJrIIDqOgRjNbY2owVt5Dt3OO62o8bKx0= +github.com/xmidt-org/candlelight v0.1.22/go.mod h1:JP//DhMaHNQYFzCEGmENEyOpmTw8wqkm1NOeE0oYw4w= +github.com/xmidt-org/eventor v1.0.23 h1:BYRLNlvFs2xdWFH2mJZxtoadV2LzXGhUsNgdd/XsbvY= +github.com/xmidt-org/eventor v1.0.23/go.mod h1:rnoyWsy2Emwa3sNMF5x0c/ykUy4sk5W+1zyLUOmP1gw= +github.com/xmidt-org/httpaux v0.4.1 h1:hvI4lZ7RacQRA/jMqUgwEQGcYm85125P4s7Y1imn6KY= +github.com/xmidt-org/httpaux v0.4.1/go.mod h1:tZJ+SBoGNCxDOLopuSqrxaCkIVAQ+aPjNRf2XfMVwJA= +github.com/xmidt-org/sallust v0.2.3 h1:NtOp/8sw0Y+HDpIhplFl+UJDNN/hgazEl+2vhkywm88= +github.com/xmidt-org/sallust v0.2.3/go.mod h1:heTYyv9B5PjMEvVASrUSoqk+T0zEu3fNNrhJp5kj+rk= +github.com/xmidt-org/touchstone v0.1.7 h1:gi3uXLDhXONe+vdVAa9xQBucMD/tJ74NMER2Lw2lo7U= +github.com/xmidt-org/touchstone v0.1.7/go.mod h1:cuukL7BhuCX6OIEhDymFnR5mRw3wBwKFdNUOzMYxE20= +github.com/xmidt-org/wrp-go/v3 v3.6.0 h1:g8qk4Xtzm7f9AslSlhv46syb9FJpDdyOSqVlUa45L7g= +github.com/xmidt-org/wrp-go/v3 v3.6.0/go.mod h1:eyMj+q/7LQ4SU6Z3s6VOwuTVSh6/DJBb2soBGBFSung= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0Jwtl01UFiyWZEMiJZBiTlv5A50zNs8lsthXqIio= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0 h1:X3ZjNp36/WlkSYx0ul2jw4PtbNEDDeLskw3VPsrpYM0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0/go.mod h1:2uL/xnOXh0CHOBFCWXz5u1A4GXLiW+0IQIzVbeOEQ0U= +go.opentelemetry.io/otel/exporters/zipkin v1.29.0 h1:rqaUJdM9ItWf6DGrelaShXnJpb8rd3HTbcZWptvcsWA= +go.opentelemetry.io/otel/exporters/zipkin v1.29.0/go.mod h1:wDIyU6DjrUYqUgnmzjWnh1HOQGZCJ6YXMIJCdMc+T9Y= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= +gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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/dealancer/validate.v2 v2.1.0 h1:XY95SZhVH1rBe8uwtnQEsOO79rv8GPwK+P3VWhQfJbA= +gopkg.in/dealancer/validate.v2 v2.1.0/go.mod h1:EipWMj8hVO2/dPXVlYRe9yKcgVd5OttpQDiM1/wZ0DE= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/apiauth/auth.go b/internal/apiauth/auth.go new file mode 100644 index 0000000..8d4183e --- /dev/null +++ b/internal/apiauth/auth.go @@ -0,0 +1,236 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package apiauth + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/xmidt-org/arrange/arrangehttp" + "github.com/xmidt-org/bascule" + "github.com/xmidt-org/bascule/basculehttp" + "github.com/xmidt-org/bascule/basculejwt" +) + +// Config is a struct that holds the configuration for the Auth middleware. +type Config struct { + // Disable is a flag that if set to true, will disable all auth. If this + // is not set and no other auth is configured, then an error will be returned. + Disable bool + + // Basic is a map of usernames to passwords. If this is set, then basic auth + // will be enabled. + // If JWT is also set, then JWT will take precedence and basic auth will be ignored. + Basic Basic + + // JWT holds the configuration for JWT based auth. + JWT JWT +} + +// JWT is a struct that holds the configuration for JWT based auth. +type JWT struct { + // KeyProvider holds the configuration for the key provider. + KeyProvider Provider + + // RequiredServiceCapabilities is a list of capabilities that are required + // to be present to accept the token. If any one of these capabilities are + // present will allow the token to be accepted. + RequiredServiceCapabilities []string +} + +// Provider contains the configuration for accessing the public keys for JWT +// verification. +type Provider struct { + // URL is the URL to get the public keys from. + URL string + + // RefreshInterval is the interval to refresh the public keys. + RefreshInterval time.Duration + + // HTTPClient is the configuration for the http client to use to get the public keys. + HTTPClient arrangehttp.ClientConfig + + // DisableAutoAddMissingAlgorithm is a flag that if set to true, will disable + // the automatic addition of missing algorithms to keys that are missing them. + // If this is not set, then the default is to add missing algorithms to keys + // that are missing the "alg" field. Sometimes the public key provider does + // not provide the "alg" field, and this function can be used to add it. + DisableAutoAddMissingAlgorithm bool +} + +// Basic is a map of usernames to passwords. +type Basic map[string]string + +// Auth is a struct that holds the auth middleware. +type Auth struct { + middleware *basculehttp.Middleware + config Config +} + +// New creates a new Auth middleware. +func New(opts ...Option) (*Auth, error) { + var err error + var auth Auth + + opts = append(opts, validate()) + + for _, opt := range opts { + if opt != nil { + if err := opt.apply(&auth); err != nil { + return nil, err + } + } + } + + ctx := context.Background() + + if auth.config.Basic != nil { + auth.middleware, err = auth.config.Basic.middleware() + if err != nil { + return nil, errors.Join(err, fmt.Errorf("error creating basic auth middleware")) + } + } + + if auth.config.JWT.KeyProvider.URL != "" { + auth.middleware, err = auth.config.JWT.middleware(ctx) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("error creating jwt middleware")) + } + } + + return &auth, err +} + +func (auth *Auth) Protected() bool { + return auth.middleware != nil +} + +func (auth *Auth) Then(h http.HandlerFunc) http.Handler { + if auth.middleware == nil { + return h + } + + return auth.middleware.ThenFunc(h) +} + +func (cfg *Basic) valid(token bascule.Token) error { + if basic, ok := token.(basculehttp.BasicToken); ok { + for u, p := range *cfg { + if basic.UserName() == u && basic.Password() == p { + return nil + } + } + } + + return bascule.ErrBadCredentials +} + +func (cfg *Basic) middleware() (*basculehttp.Middleware, error) { + tp, err := basculehttp.NewAuthorizationParser( + basculehttp.WithBasic(), + ) + if err != nil { + return nil, err + } + + m, err := basculehttp.NewMiddleware( + basculehttp.UseAuthenticator( + + basculehttp.NewAuthenticator( + bascule.WithTokenParsers(tp), + bascule.WithValidators( + bascule.AsValidator[*http.Request](cfg.valid), + ), + ), + ), + ) + + return m, err +} + +func (cfg *JWT) middleware(ctx context.Context) (*basculehttp.Middleware, error) { + keys, err := cfg.KeyProvider.toKeySet(ctx) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("error getting public keys")) + } + + jwtp, err := basculejwt.NewTokenParser(jwt.WithKeySet(keys)) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("error creating token parser")) + } + + tp, err := basculehttp.NewAuthorizationParser( + basculehttp.WithScheme(basculehttp.SchemeBearer, jwtp), + ) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("error creating authorization parser")) + } + + m, err := basculehttp.NewMiddleware( + basculehttp.UseAuthenticator( + basculehttp.NewAuthenticator( + bascule.WithTokenParsers(tp), + bascule.WithValidators( + bascule.AsValidator[*http.Request](cfg.valid), + ), + ), + ), + ) + + return m, err +} + +// just checking for at least one capabiilty +func (cfg *JWT) valid(token bascule.Token) error { + _, ok := token.(basculejwt.Claims) + if !ok { + return bascule.ErrBadCredentials + } + + if len(cfg.RequiredServiceCapabilities) == 0 { + return nil + } + + capabilities, _ := bascule.GetCapabilities(token) + for _, capability := range capabilities { + for _, serviceCapability := range cfg.RequiredServiceCapabilities { + if capability == serviceCapability { + return nil + } + } + } + + return bascule.ErrUnauthorized +} + +func (cfg *Provider) toKeySet(ctx context.Context) (jwk.Set, error) { + cache := jwk.NewCache(ctx) + + opts := []jwk.RegisterOption{ + jwk.WithRefreshInterval(cfg.RefreshInterval), + } + + if !cfg.DisableAutoAddMissingAlgorithm { + opts = append(opts, jwk.WithPostFetcher(mapMissingAlgorithms(ctx))) + } + + client, err := cfg.HTTPClient.NewClient() + if err != nil { + return nil, err + } + + opts = append(opts, jwk.WithHTTPClient(client)) + + err = cache.Register(cfg.URL, opts...) + if err != nil { + return nil, err + } + + return jwk.NewCachedSet(cache, cfg.URL), err +} diff --git a/internal/apiauth/auth_test.go b/internal/apiauth/auth_test.go new file mode 100644 index 0000000..ebc728f --- /dev/null +++ b/internal/apiauth/auth_test.go @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package apiauth + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/xmidt-org/bascule" +) + +type MockTokenProvider struct { + mock.Mock +} + +func NewMockTokenProvider() *MockTokenProvider { return &MockTokenProvider{} } + +func (m *MockTokenProvider) GetToken() (string, error) { + args := m.Called() + return args.Get(0).(string), args.Error(1) +} + +type AuthTestSuite struct { + suite.Suite + + audience []string + jwtID string + issuer string + + expiration time.Time + issuedAt time.Time + notBefore time.Time + subject string + + capabilities []string + allowedResources map[string]any + version string + + testKeyPriv jwk.Key + testKeyPub jwk.Key + + testJWT jwt.Token + signedJWT []byte + + tokenProvider *MockTokenProvider +} + +func (suite *AuthTestSuite) initializeKey() { + var err error + suite.testKeyPriv, err = jwk.ParseKey([]byte(`{ + "p": "7HMYtb-1dKyDp1OkdKc9WDdVMw3vtiiKDyuyRwnnwMOoYLPYxqE0CUMzw8_zXuzq7WJAmGiFd5q7oVzkbHzrtQ", + "kty": "RSA", + "q": "5253lCAgBLr8SR_VzzDtk_3XTHVmVIgniajMl7XM-ttrUONV86DoIm9VBx6ywEKpj5Xv3USBRNlpf8OXqWVhPw", + "d": "G7RLbBiCkiZuepbu46G0P8J7vn5l8G6U78gcMRdEhEsaXGZz_ZnbqjW6u8KI_3akrBT__GDPf8Hx8HBNKX5T9jNQW0WtJg1XnwHOK_OJefZl2fnx-85h3tfPD4zI3m54fydce_2kDVvqTOx_XXdNJD7v5TIAgvCymQv7qvzQ0VE", + "e": "AQAB", + "use": "sig", + "kid": "test", + "qi": "a_6YlMdA9b6piRodA0MR7DwjbALlMan19wj_VkgZ8Xoilq68sGaV2CQDoAdsTW9Mjt5PpCxvJawz0AMr6LIk9w", + "dp": "s55HgiGs_YHjzSOsBXXaEv6NuWf31l_7aMTf_DkZFYVMjpFwtotVFUg4taJuFYlSeZwux9h2s0IXEOCZIZTQFQ", + "alg": "RS256", + "dq": "M79xoX9laWleDAPATSnFlbfGsmP106T2IkPKK4oNIXJ6loWerHEoNrrqKkNk-LRvMZn3HmS4-uoaOuVDPi9bBQ", + "n": "1cHjMu7H10hKxnoq3-PJT9R25bkgVX1b39faqfecC82RMcD2DkgCiKGxkCmdUzuebpmXCZuxp-rVVbjrnrI5phAdjshZlkHwV0tyJOcerXsPgu4uk_VIJgtLdvgUAtVEd8-ZF4Y9YNOAKtf2AHAoRdP0ZVH7iVWbE6qU-IN2los" +}`)) + suite.Require().NoError(err) + + suite.testKeyPub, err = suite.testKeyPriv.PublicKey() + suite.Require().NoError(err) +} + +func (suite *AuthTestSuite) initializeClaims() { + suite.audience = []string{"test-audience"} + suite.jwtID = "test-jwt" + suite.issuer = "test-issuer" + + suite.issuedAt = time.Now().Add(-time.Second).Round(time.Second).UTC() + suite.expiration = suite.issuedAt.Add(time.Hour) + suite.notBefore = suite.issuedAt.Add(-time.Hour) + + suite.subject = "test-subject" + + suite.capabilities = []string{ + "example-capability", + "alt-capability", + } + + suite.allowedResources = make(map[string]any) + suite.allowedResources["allowedPartners"] = []string{"comcast"} + + suite.version = "2.0" +} + +func (suite *AuthTestSuite) createJWT() { + var err error + suite.testJWT, err = jwt.NewBuilder(). + Audience(suite.audience). + Subject(suite.subject). + IssuedAt(suite.issuedAt). + Expiration(suite.expiration). + NotBefore(suite.notBefore). + JwtID(suite.jwtID). + Issuer(suite.issuer). + Claim("capabilities", suite.capabilities). + Claim("allowedResources", suite.allowedResources). + Claim("version", suite.version). + Build() + + suite.Require().NoError(err) + + suite.signedJWT, err = jwt.Sign(suite.testJWT, jwt.WithKey(jwa.RS256, suite.testKeyPriv)) + suite.Require().NoError(err) +} + +func (suite *AuthTestSuite) SetupSuite() { + suite.initializeKey() + suite.initializeClaims() + suite.createJWT() + suite.tokenProvider = NewMockTokenProvider() +} + +func TestAuth(t *testing.T) { + suite.Run(t, new(AuthTestSuite)) +} + +func (suite *AuthTestSuite) TestBasicAuth() { + username := "some-username" + config := Config{ + Basic: Basic{ + username: "some-password", + }, + } + + auth, err := New( + WithConfig(config), + ) + suite.NoError(err) + + var reached int + h := auth.middleware.ThenFunc( + func(w http.ResponseWriter, r *http.Request) { + t, _ := bascule.GetFrom(r) + suite.Equal(username, t.Principal()) + reached++ + }, + ) + + goodRequest := httptest.NewRequest("GET", "/", nil) + goodRequest.SetBasicAuth(username, config.Basic[username]) + response := httptest.NewRecorder() + h.ServeHTTP(response, goodRequest) + suite.Equal(http.StatusOK, response.Code) + suite.Equal(1, reached) + + badRequest := httptest.NewRequest("GET", "/", nil) + badRequest.SetBasicAuth(username, "some-bad-password") + response = httptest.NewRecorder() + h.ServeHTTP(response, badRequest) + suite.Equal(http.StatusUnauthorized, response.Code) + suite.Equal(1, reached) +} + +func (suite *AuthTestSuite) TestJwtAuth() { + set := jwk.NewSet() + err := set.AddKey(suite.testKeyPub) + suite.Require().NoError(err) + setBytes, err := json.Marshal(set) + suite.Require().NoError(err) + suite.Require().NotNil(setBytes) + + // Create a test HTTP server that returns the JWK set + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(setBytes) + })) + defer server.Close() + + config := Config{ + JWT: JWT{ + KeyProvider: Provider{ + URL: server.URL, + RefreshInterval: 15 * time.Minute, + }, + }, + } + + auth, err := New( + WithConfig(config), + ) + suite.NoError(err) + + var reached int + h := auth.middleware.ThenFunc( + func(w http.ResponseWriter, r *http.Request) { + t, _ := bascule.GetFrom(r) + suite.Equal(suite.subject, t.Principal()) + reached++ + }, + ) + + goodRequest := httptest.NewRequest("GET", "/", nil) + goodRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(suite.signedJWT))) + response := httptest.NewRecorder() + h.ServeHTTP(response, goodRequest) + suite.Equal(http.StatusOK, response.Code) + suite.Equal(1, reached) + + badRequest := httptest.NewRequest("GET", "/", nil) + badRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", "some bad token")) + response = httptest.NewRecorder() + h.ServeHTTP(response, badRequest) + suite.Equal(http.StatusBadRequest, response.Code) + suite.Equal(1, reached) + + // no matching capabilities + auth.config.JWT.RequiredServiceCapabilities = []string{"some-other-capability"} + forbiddenRequest := httptest.NewRequest("GET", "/", nil) + forbiddenRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(suite.signedJWT))) + response = httptest.NewRecorder() + h.ServeHTTP(response, forbiddenRequest) + suite.Equal(http.StatusForbidden, response.Code) + suite.Equal(1, reached) + + // matching capabilities + auth.config.JWT.RequiredServiceCapabilities = []string{"example-capability"} + authorizedRequest := httptest.NewRequest("GET", "/", nil) + authorizedRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(suite.signedJWT))) + response = httptest.NewRecorder() + h.ServeHTTP(response, authorizedRequest) + suite.Equal(http.StatusOK, response.Code) + suite.Equal(2, reached) +} diff --git a/internal/apiauth/errors.go b/internal/apiauth/errors.go new file mode 100644 index 0000000..0e96db0 --- /dev/null +++ b/internal/apiauth/errors.go @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package apiauth + +import "errors" + +var ( + // ErrInvalidConfig is returned when the config is invalid. + ErrInvalidConfig = errors.New("invalid config") +) diff --git a/internal/apiauth/fx.go b/internal/apiauth/fx.go new file mode 100644 index 0000000..fb31895 --- /dev/null +++ b/internal/apiauth/fx.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package apiauth + +import ( + "go.uber.org/fx" +) + +type AuthIn struct { + fx.In + Config Config +} + +type AuthOut struct { + fx.Out + Auth *Auth +} + +var Module = fx.Module("auth", + fx.Provide( + func(in AuthIn) (AuthOut, error) { + auth, err := New( + WithConfig(in.Config), + ) + + return AuthOut{ + Auth: auth}, err + }, + ), +) diff --git a/internal/apiauth/mapAlgs.go b/internal/apiauth/mapAlgs.go new file mode 100644 index 0000000..13c37ae --- /dev/null +++ b/internal/apiauth/mapAlgs.go @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package apiauth + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "fmt" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" +) + +// mapMissingAlgorithms is a jwk.PostFetchFunc that will add missing +// algorithms to keys that are missing them. This is useful when +// you have a key that is missing the "alg" field, but you know what +// the algorithm should be. Sometimes the public key provider does not +// provide the "alg" field, and this function can be used to add it. +func mapMissingAlgorithms(ctx context.Context) jwk.PostFetchFunc { + return func(url string, keySet jwk.Set) (jwk.Set, error) { + newKeys := jwk.NewSet() + keys := keySet.Keys(ctx) + for keys.Next(ctx) { + key := keys.Pair().Value.(jwk.Key) + if key.Algorithm().String() != "" { + err := newKeys.AddKey(key) + if err != nil { + return keySet, err + } + continue + } + + algs, err := keyToAlgs(key) + if err != nil { + return keySet, err + } + + for _, alg := range algs { + keyDup, err := key.Clone() + if err != nil { + return keySet, err + } + + err = keyDup.Set(jwk.AlgorithmKey, alg) + if err != nil { + return keySet, err + } + + err = newKeys.AddKey(keyDup) + if err != nil { + return keySet, err + } + } + } + return newKeys, nil + } +} + +// keyToAlgs is a helper function that will return the algorithms that are +// supported by the key. +func keyToAlgs(key jwk.Key) ([]jwa.SignatureAlgorithm, error) { + kt := key.KeyType() + + switch kt { + case jwa.RSA: + return []jwa.SignatureAlgorithm{jwa.RS256, jwa.RS384, jwa.RS512, jwa.PS256, jwa.PS384, jwa.PS512}, nil + case jwa.OKP: + return []jwa.SignatureAlgorithm{jwa.EdDSA}, nil + case jwa.EC: + var raw any + err := key.Raw(&raw) + if err != nil { + return nil, err + } + switch k := raw.(type) { + case *ecdsa.PublicKey: + switch k.Curve { + case elliptic.P256(): + return []jwa.SignatureAlgorithm{jwa.ES256}, nil + case elliptic.P384(): + return []jwa.SignatureAlgorithm{jwa.ES384}, nil + case elliptic.P521(): + return []jwa.SignatureAlgorithm{jwa.ES512}, nil + } + case *ecdsa.PrivateKey: + switch k.Curve { + case elliptic.P256(): + return []jwa.SignatureAlgorithm{jwa.ES256}, nil + case elliptic.P384(): + return []jwa.SignatureAlgorithm{jwa.ES384}, nil + case elliptic.P521(): + return []jwa.SignatureAlgorithm{jwa.ES512}, nil + } + default: + } + default: + } + + return nil, fmt.Errorf("unsupported key type %s", kt) +} diff --git a/internal/apiauth/mapAlgs_test.go b/internal/apiauth/mapAlgs_test.go new file mode 100644 index 0000000..005d1a6 --- /dev/null +++ b/internal/apiauth/mapAlgs_test.go @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package apiauth + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mustGenerateKey(what string) jwk.Key { + list := strings.Split(what, ".") + if len(list) < 3 { + panic("invalid what. Either 'rsa.public.kid123' or 'rsa.private.kid123' or 'ec.256.public.kid123' or 'ec.256.private.kid123'") + } + + var generic any + var kid string + if len(list) == 3 { + var kt, which string + kt, which, kid = list[0], list[1], list[2] + + if kt != "rsa" { + panic("invalid key type") + } + + raw, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + + generic = raw + if which == "public" { + generic = &raw.PublicKey + } + } else { + var kt, bits, which string + kt, bits, which, kid = list[0], list[1], list[2], list[3] + + if kt != "ec" { + panic("invalid key type") + } + + var ec elliptic.Curve + switch bits { + case "256": + ec = elliptic.P256() + case "384": + ec = elliptic.P384() + case "512", "521": + ec = elliptic.P521() + default: + panic("invalid bits") + } + + raw, err := ecdsa.GenerateKey(ec, rand.Reader) + if err != nil { + panic(err) + } + + generic = raw + if which == "public" { + generic = &raw.PublicKey + } + } + + key, err := jwk.FromRaw(generic) + if err != nil { + panic(err) + } + + err = key.Set("kid", kid) + if err != nil { + panic(err) + } + + if key == nil { + panic("key is nil") + } + return key +} + +func TestExpandKeySetByAlgorithm(t *testing.T) { + tests := []struct { + key jwk.Key + want []jwa.SignatureAlgorithm + }{ + { + key: mustGenerateKey("rsa.private.kid123"), + want: []jwa.SignatureAlgorithm{ + jwa.RS256, + jwa.RS384, + jwa.RS512, + jwa.PS256, + jwa.PS384, + jwa.PS512, + }, + }, { + key: mustGenerateKey("ec.256.private.kid123"), + want: []jwa.SignatureAlgorithm{jwa.ES256}, + }, { + key: mustGenerateKey("ec.384.private.kid123"), + want: []jwa.SignatureAlgorithm{jwa.ES384}, + }, { + key: mustGenerateKey("ec.512.private.kid123"), + want: []jwa.SignatureAlgorithm{jwa.ES512}, + }, { + key: mustGenerateKey("ec.256.public.kid123"), + want: []jwa.SignatureAlgorithm{jwa.ES256}, + }, { + key: mustGenerateKey("ec.384.public.kid123"), + want: []jwa.SignatureAlgorithm{jwa.ES384}, + }, { + key: mustGenerateKey("ec.512.public.kid123"), + want: []jwa.SignatureAlgorithm{jwa.ES512}, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + ks := jwk.NewSet() + require.NotNil(ks) + + err := ks.AddKey(test.key) + require.NoError(err) + + ctx := context.Background() + + f := mapMissingAlgorithms(ctx) + require.NotNil(f) + + newKS, err := f("localhost", ks) + require.NoError(err) + require.NotNil(newKS) + + keys := newKS.Keys(ctx) + require.NotNil(keys) + + var got []jwa.SignatureAlgorithm + for keys.Next(ctx) { + key := keys.Pair().Value.(jwk.Key) + alg, ok := key.Get(jwk.AlgorithmKey) + require.True(ok) + + got = append(got, alg.(jwa.SignatureAlgorithm)) + } + + assert.ElementsMatch(test.want, got) + + // Now check to make sure if the algorithm is already set, we don't + // overwrite it. + again, err := f("localhost", newKS) + require.NoError(err) + require.NotNil(again) + + assert.ElementsMatch(test.want, got) + }) + } +} + +func TestGetKeys(t *testing.T) { + // Create a test JWK set + jwkSet := `{ + "keys": [ + { + "kty": "EC", + "crv": "P-256", + "x": "f83OJ3D2xF4a8u6B3T1t5w", + "y": "x_FEzRu9Q5R5t5w", + "alg": "ES256", + "kid": "1" + } + ] + }` + + // Create a test HTTP server that returns the JWK set + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(jwkSet)) + })) + defer server.Close() + + // Create a mock provider + provider := Provider{ + URL: server.URL, + RefreshInterval: 15 * time.Minute, + } + + // Call the getKeys function + ctx := context.Background() + keySet, err := provider.toKeySet(ctx) + + // Verify the results + require.NoError(t, err) + require.NotNil(t, keySet) + + keys := keySet.Keys(ctx) + require.True(t, keys.Next(ctx)) + + key := keys.Pair().Value.(jwk.Key) + assert.Equal(t, "EC", key.KeyType().String()) + assert.Equal(t, "ES256", key.Algorithm().String()) + assert.Equal(t, "1", key.KeyID()) +} diff --git a/internal/apiauth/options.go b/internal/apiauth/options.go new file mode 100644 index 0000000..18511fc --- /dev/null +++ b/internal/apiauth/options.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package apiauth + +import ( + "fmt" + "reflect" +) + +// Option is an interface that is used to apply options to the Auth struct. +type Option interface { + apply(*Auth) error +} + +// optionFunc is a function that is used to apply options to the Auth struct. +type optionFunc func(*Auth) error + +func (of optionFunc) apply(a *Auth) error { + return of(a) +} + +func WithConfig(c Config) optionFunc { + return func(a *Auth) error { + a.config = c + return nil + } +} + +//------------------------------------------------------------------------------ + +func validate() optionFunc { + return func(a *Auth) error { + if a.config.Disable { + expect := Config{ + Disable: true, + } + + if !reflect.DeepEqual(a.config, expect) { + return fmt.Errorf("%w: disable cannot have additional values", ErrInvalidConfig) + } + } + + if reflect.DeepEqual(a.config, Config{}) { + return fmt.Errorf("%w: empty configuration is not valid, set 'disable' to true if no validation is wanted", ErrInvalidConfig) + } + + return nil + } +} diff --git a/internal/log/const.go b/internal/log/const.go new file mode 100644 index 0000000..12b343b --- /dev/null +++ b/internal/log/const.go @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package log + +const ( + // common log fields + ErrorCode = "error.code" + ErrorStackTrace = "error.stack_trace" + ErrorMessage = "error.message" + RequestBody = "http.request.body.content" + + // fides specific fields + // TBD +) diff --git a/internal/metrics/fx.go b/internal/metrics/fx.go new file mode 100644 index 0000000..18ab857 --- /dev/null +++ b/internal/metrics/fx.go @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package metrics + +import ( + "fmt" + "strconv" + "strings" + + "github.com/prometheus/client_golang/prometheus" + "github.com/xmidt-org/touchstone/touchkit" + "go.uber.org/fx" +) + +type metricType int + +const ( + COUNTER metricType = 1 + GAUGE metricType = 2 + HISTOGRAM metricType = 3 +) + +type metricDefinition struct { + Type metricType + Name string // the metric name (prometheus.CounterOpts.Name, etc) + Help string // the metric help (prometheus.CounterOpts.Help, etc) + Labels string // a comma separated list of labels that are whitespace trimmed + Buckets string // a comma separated list of labels that are whitespace trimmed +} + +var fxMetrics = []metricDefinition{ + { + Type: COUNTER, + Name: "oking_request_count", + Help: "The number of times the oking request has been attempted.", + Labels: "outcome, partnerid, status_code", + }, + + { + Type: HISTOGRAM, + Name: "oking_call_duration", + Help: "The duration of call.", + Labels: "outcome, partnerid, status_code", + Buckets: "10, 100, 1000, 5000, 10000, 100000, 500000, 1000000, 2000000", + }, +} + +func Provide() fx.Option { + var opts []fx.Option // nolint: prealloc + + for _, m := range fxMetrics { + labels := strings.Split(m.Labels, ",") + for i := range labels { + labels[i] = strings.TrimSpace(labels[i]) + } + + var opt fx.Option + + switch m.Type { + case COUNTER: + opt = touchkit.Counter( + prometheus.CounterOpts{ + Name: m.Name, + Help: m.Help, + }, + labels...) + case GAUGE: + opt = touchkit.Gauge( + prometheus.GaugeOpts{ + Name: m.Name, + Help: m.Help, + }, + labels...) + case HISTOGRAM: + buckets := strings.Split(m.Buckets, ",") + bucketLimits := make([]float64, len(buckets)) + for i := range buckets { + bucketLimit, err := strconv.ParseFloat(strings.TrimSpace(buckets[i]), 64) + if err != nil { + panic(fmt.Sprintf("bucket has non-numeric value '%s'", buckets[i])) + } + bucketLimits[i] = bucketLimit + } + opt = touchkit.Histogram( + prometheus.HistogramOpts{ + Name: m.Name, + Help: m.Help, + Buckets: bucketLimits, + }, + labels...) + default: + panic(fmt.Sprintf("unknown metric type %d for '%s'", m.Type, m.Name)) + } + + if opt == nil { + panic(fmt.Sprintf("failed to create metric '%s'", m.Name)) + } + + opts = append(opts, opt) + } + + return fx.Options(opts...) +} diff --git a/internal/metrics/tags.go b/internal/metrics/tags.go new file mode 100644 index 0000000..1e56abb --- /dev/null +++ b/internal/metrics/tags.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package metrics + +// events +const ( + SigningRequestReceived = "signing_request_received" +) + +// errors +const ( + Panic = "panic" +) + +func GetUnknownTagIfEmpty(tag string) string { + if tag == "" { + return "unknown" + } + return tag +} diff --git a/internal/metrics/tags_test.go b/internal/metrics/tags_test.go new file mode 100644 index 0000000..05a3fe4 --- /dev/null +++ b/internal/metrics/tags_test.go @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package metrics + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetUnknownTagIfEmpty(t *testing.T) { + assert.Equal(t, "unknown", GetUnknownTagIfEmpty("")) + assert.Equal(t, "some-tag", GetUnknownTagIfEmpty("some-tag")) +} diff --git a/internal/oker/events.go b/internal/oker/events.go new file mode 100644 index 0000000..d170abf --- /dev/null +++ b/internal/oker/events.go @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package oker + +import ( + "fmt" + "strings" + "time" +) + +// OkEvent is the event that is sent about an ok request. +type OkEvent struct { + // At holds the time when the request was made. + At time.Time + + // PartnerID is the partner id of the request. + PartnerID string + + // Duration is the time needed to ok the request. + Duration time.Duration + + // StatusCode is the resulting http status code. + StatusCode int + + // Error is the resulting error. + Err error +} + +func (e OkEvent) String() string { + buf := strings.Builder{} + + buf.WriteString("oker.OkEvent{\n") + buf.WriteString(fmt.Sprintf(" At: %s\n", e.At.Format(time.RFC3339))) + buf.WriteString(fmt.Sprintf(" Duration: %s\n", e.Duration.String())) + buf.WriteString(fmt.Sprintf(" StatusCode: %d\n", e.StatusCode)) + buf.WriteString(fmt.Sprintf(" Err: %v\n", e.Err)) + buf.WriteString("}") + + return buf.String() +} + +// OkEventListener is the interface that must be implemented by types that +// want to receive FetchEvent notifications. +type OkEventListener interface { + OnOkEvent(OkEvent) +} + +// OkEventListenerFunc is a function type that implements OkEventListener. +// It can be used as an adapter for functions that need to implement the +// OkEventListener interface. +type OkEventListenerFunc func(OkEvent) + +func (f OkEventListenerFunc) OnEventEvent(e OkEvent) { + f(e) +} diff --git a/internal/oker/fx.go b/internal/oker/fx.go new file mode 100644 index 0000000..505bc2a --- /dev/null +++ b/internal/oker/fx.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package oker + +import ( + kit "github.com/go-kit/kit/metrics" + "go.uber.org/fx" + "go.uber.org/zap" +) + +type telemetryIn struct { + fx.In + + Logger *zap.Logger + Counter kit.Counter `name:"oking_request_count"` + Duration kit.Histogram `name:"oking_call_duration"` +} + +var Module = fx.Module("oker", + fx.Provide( + func(in telemetryIn) *telemetry { + return &telemetry{ + logger: in.Logger, + counter: in.Counter, + duration: in.Duration, + } + }), + fx.Provide( + func(cfg Config, t *telemetry) (*Server, error) { + a, err := New( + WithConfig(cfg), + AddOkEventListener(t), + ) + + return a, err + }, + ), +) diff --git a/internal/oker/oker.go b/internal/oker/oker.go new file mode 100644 index 0000000..a1cf66c --- /dev/null +++ b/internal/oker/oker.go @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package oker + +import ( + "net/http" + "time" + + "github.com/xmidt-org/eventor" +) + +type Config struct { + Name string +} + +type Server struct { + config Config + okEventListeners eventor.Eventor[OkEventListener] +} + +type Option interface { + apply(*Server) error +} + +type optionFunc func(*Server) error + +func (f optionFunc) apply(s *Server) error { + return f(s) +} + +func New(opts ...Option) (*Server, error) { + var s Server + + for _, opt := range opts { + if opt != nil { + if err := opt.apply(&s); err != nil { + return nil, err + } + } + } + + return &s, nil +} + +func (s *Server) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + var e OkEvent + + e.At = time.Now() + e.StatusCode = http.StatusOK + resp.WriteHeader(http.StatusOK) + + s.okEventListeners.Visit(func(listener OkEventListener) { + listener.OnOkEvent(e) + }) +} diff --git a/internal/oker/options.go b/internal/oker/options.go new file mode 100644 index 0000000..e4e36b1 --- /dev/null +++ b/internal/oker/options.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package oker + +func WithConfig(c Config) Option { + return optionFunc(func(s *Server) error { + s.config = c + return nil + }) +} + +// AddOkListener adds a listener for oking events. If the optional cancel +// parameter is provided, it is set to a function that can be used to cancel +// the listener. +func AddOkEventListener(listener OkEventListener, cancel ...*func()) Option { + return optionFunc(func(s *Server) error { + cncl := s.okEventListeners.Add(listener) + if len(cancel) > 0 && cancel[0] != nil { + *cancel[0] = cncl + } + return nil + }) +} diff --git a/internal/oker/telemetry.go b/internal/oker/telemetry.go new file mode 100644 index 0000000..6493e31 --- /dev/null +++ b/internal/oker/telemetry.go @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package oker + +import ( + "strconv" + "time" + + kit "github.com/go-kit/kit/metrics" + "go.uber.org/zap" +) + +type telemetry struct { + counter kit.Counter + duration kit.Histogram + logger *zap.Logger +} + +func (t *telemetry) OnOkEvent(e OkEvent) { + outcome := "failure" + if e.Err == nil { + outcome = "success" + } + + fields := []zap.Field{ + zap.String("fetched_at", e.At.Format(time.RFC3339)), + zap.Duration("duration", e.Duration), + zap.Int("status_code", e.StatusCode), + zap.String("outcome", outcome), + zap.Error(e.Err), + } + + if e.Err == nil { + t.logger.Info("oking request", fields...) + } else { + t.logger.Error("oking request", fields...) + } + + labels := []string{ + "outcome", outcome, + "status_code", strconv.Itoa(e.StatusCode), + "partnerid", e.PartnerID, + } + + t.counter.With(labels...).Add(1) + t.duration.With(labels...).Observe(float64(e.Duration.Milliseconds())) +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..3e14eb4 --- /dev/null +++ b/logger.go @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package skeleton + +import ( + "github.com/xmidt-org/sallust" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Create the logger and configure it based on if the program is in +// debug mode or normal mode. +func provideLogger(cli *CLI, cfg sallust.Config) (*zap.Logger, error) { + if cli.Dev { + cfg.Level = "DEBUG" + cfg.Development = true + cfg.Encoding = "console" + cfg.EncoderConfig = sallust.EncoderConfig{ + TimeKey: "T", + LevelKey: "L", + NameKey: "N", + CallerKey: "C", + FunctionKey: zapcore.OmitKey, + MessageKey: "M", + StacktraceKey: "S", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: "capitalColor", + EncodeTime: "RFC3339", + EncodeDuration: "string", + EncodeCaller: "short", + } + cfg.OutputPaths = []string{"stderr"} + cfg.ErrorOutputPaths = []string{"stderr"} + } + return cfg.Build() +} diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..e8f98de --- /dev/null +++ b/routes.go @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package skeleton + +import ( + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/xmidt-org/arrange/arrangehttp" + "github.com/xmidt-org/arrange/arrangepprof" + "github.com/xmidt-org/httpaux" + "github.com/xmidt-org/skeleton/internal/apiauth" + "github.com/xmidt-org/skeleton/internal/oker" + "github.com/xmidt-org/touchstone/touchhttp" + "go.uber.org/fx" +) + +type RoutesIn struct { + fx.In + PrimaryMetrics touchhttp.ServerInstrumenter `name:"servers.primary.metrics"` + AlternateMetrics touchhttp.ServerInstrumenter `name:"servers.alternate.metrics"` + Routes Routes + Oker *oker.Server + ApiAuth *apiauth.Auth +} + +type RoutesOut struct { + fx.Out + Primary arrangehttp.Option[http.Server] `group:"servers.primary.options"` + Alternate arrangehttp.Option[http.Server] `group:"servers.alternate.options"` +} + +// The name should be 'primary' or 'alternate'. +func provideCoreEndpoints() fx.Option { + return fx.Provide( + fx.Annotated{ + Name: "servers.primary.metrics", + Target: touchhttp.ServerBundle{}.NewInstrumenter( + touchhttp.ServerLabel, "primary", + ), + }, + fx.Annotated{ + Name: "servers.alternate.metrics", + Target: touchhttp.ServerBundle{}.NewInstrumenter( + touchhttp.ServerLabel, "alternate", + ), + }, + func(in RoutesIn) RoutesOut { + return RoutesOut{ + Primary: provideCoreOption("primary", in), + Alternate: provideCoreOption("alternate", in), + } + }, + ) +} + +func provideCoreOption(server string, in RoutesIn) arrangehttp.Option[http.Server] { + return arrangehttp.AsOption[http.Server]( + func(s *http.Server) { + mux := chi.NewMux() + if strings.ToLower(in.Routes.Oker.Server) == server { + mux.Method("GET", in.Routes.Oker.Path, + in.ApiAuth.Then(in.Oker.ServeHTTP)) + } + if server == "primary" { + s.Handler = in.PrimaryMetrics.Then(mux) + } else { + s.Handler = in.AlternateMetrics.Then(mux) + } + }, + ) + +} + +func provideHealthCheck() fx.Option { + return fx.Provide( + fx.Annotated{ + Name: "servers.health.metrics", + Target: touchhttp.ServerBundle{}.NewInstrumenter( + touchhttp.ServerLabel, "health", + ), + }, + fx.Annotate( + func(metrics touchhttp.ServerInstrumenter, path HealthPath) arrangehttp.Option[http.Server] { + return arrangehttp.AsOption[http.Server]( + func(s *http.Server) { + mux := chi.NewMux() + mux.Method("GET", string(path), httpaux.ConstantHandler{ + StatusCode: http.StatusOK, + }) + s.Handler = metrics.Then(mux) + }, + ) + }, + fx.ParamTags(`name:"servers.health.metrics"`), + fx.ResultTags(`group:"servers.health.options"`), + ), + ) +} + +func provideMetricEndpoint() fx.Option { + return fx.Provide( + fx.Annotate( + func(metrics touchhttp.Handler, path MetricsPath) arrangehttp.Option[http.Server] { + return arrangehttp.AsOption[http.Server]( + func(s *http.Server) { + mux := chi.NewMux() + mux.Method("GET", string(path), metrics) + s.Handler = mux + }, + ) + }, + fx.ResultTags(`group:"servers.metrics.options"`), + ), + ) +} + +func providePprofEndpoint() fx.Option { + return fx.Provide( + fx.Annotate( + func(pathPrefix PprofPathPrefix) arrangehttp.Option[http.Server] { + return arrangehttp.AsOption[http.Server]( + func(s *http.Server) { + s.Handler = arrangepprof.HTTP{ + PathPrefix: string(pathPrefix), + }.New() + }, + ) + }, + fx.ResultTags(`group:"servers.pprof.options"`), + ), + ) +} diff --git a/skeleton.go b/skeleton.go new file mode 100644 index 0000000..65df4cc --- /dev/null +++ b/skeleton.go @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package skeleton + +import ( + "fmt" + "os" + + "github.com/alecthomas/kong" + "github.com/goschtalt/goschtalt" + _ "github.com/goschtalt/goschtalt/pkg/typical" + _ "github.com/goschtalt/yaml-decoder" + _ "github.com/goschtalt/yaml-encoder" + "github.com/xmidt-org/arrange/arrangehttp" + "github.com/xmidt-org/candlelight" + "github.com/xmidt-org/sallust" + "github.com/xmidt-org/skeleton/internal/apiauth" + "github.com/xmidt-org/skeleton/internal/metrics" + "github.com/xmidt-org/skeleton/internal/oker" + "github.com/xmidt-org/touchstone" + "github.com/xmidt-org/touchstone/touchhttp" + + "go.uber.org/fx" + "go.uber.org/fx/fxevent" + "go.uber.org/zap" +) + +const ( + applicationNamespace = "xmidt" + applicationName = "skeleton" +) + +// These match what goreleaser provides. +var ( + commit = "undefined" + version = "undefined" + date = "undefined" + builtBy = "undefined" +) + +// CLI is the structure that is used to capture the command line arguments. +type CLI struct { + Dev bool `optional:"" short:"d" help:"Run in development mode."` + Show bool `optional:"" short:"s" help:"Show the configuration and exit."` + Graph string `optional:"" short:"g" help:"Output the dependency graph to the specified file."` + Files []string `optional:"" short:"f" help:"Specific configuration files or directories."` +} + +func Main(args []string, run bool) error { // nolint: funlen + var ( + gscfg *goschtalt.Config + + // Capture the dependency tree in case we need to debug something. + g fx.DotGraph + + // Capture the command line arguments. + cli *CLI + ) + + app := fx.New( + fx.Supply(cliArgs(args)), + fx.Populate(&g), + fx.Populate(&gscfg), + fx.Populate(&cli), + + fx.WithLogger(func(log *zap.Logger) fxevent.Logger { + return &fxevent.ZapLogger{Logger: log} + }), + + fx.Provide( + provideCLI, + provideLogger, + provideConfig, + goschtalt.UnmarshalFunc[sallust.Config]("logging"), + goschtalt.UnmarshalFunc[candlelight.Config]("tracing"), + goschtalt.UnmarshalFunc[touchstone.Config]("prometheus"), + goschtalt.UnmarshalFunc[touchhttp.Config]("prometheus_handler"), + goschtalt.UnmarshalFunc[HealthPath]("servers.health.path", goschtalt.Optional()), + goschtalt.UnmarshalFunc[MetricsPath]("servers.metrics.path", goschtalt.Optional()), + goschtalt.UnmarshalFunc[PprofPathPrefix]("servers.pprof.path", goschtalt.Optional()), + goschtalt.UnmarshalFunc[Routes]("routes"), + goschtalt.UnmarshalFunc[oker.Config]("oker"), + goschtalt.UnmarshalFunc[apiauth.Config]("auth", goschtalt.Optional()), + // fx.Annotated{ + // Name: "tracing_initial_config", + // Target: goschtalt.UnmarshalFunc[candlelight.Config]("tracing"), + // }, + fx.Annotated{ + Name: "servers.health.config", + Target: goschtalt.UnmarshalFunc[arrangehttp.ServerConfig]("servers.health.http", goschtalt.Optional()), + }, + fx.Annotated{ + Name: "servers.metrics.config", + Target: goschtalt.UnmarshalFunc[arrangehttp.ServerConfig]("servers.metrics.http", goschtalt.Optional()), + }, + fx.Annotated{ + Name: "servers.pprof.config", + Target: goschtalt.UnmarshalFunc[arrangehttp.ServerConfig]("servers.pprof.http", goschtalt.Optional()), + }, + fx.Annotated{ + Name: "servers.primary.config", + Target: goschtalt.UnmarshalFunc[arrangehttp.ServerConfig]("servers.primary.http", goschtalt.Optional()), + }, + fx.Annotated{ + Name: "servers.alternate.config", + Target: goschtalt.UnmarshalFunc[arrangehttp.ServerConfig]("servers.alternate.http", goschtalt.Optional()), + }, + + candlelight.New, + ), + + provideCoreEndpoints(), + provideMetricEndpoint(), + provideHealthCheck(), + providePprofEndpoint(), + + arrangehttp.ProvideServer("servers.health"), + arrangehttp.ProvideServer("servers.metrics"), + arrangehttp.ProvideServer("servers.pprof"), + arrangehttp.ProvideServer("servers.primary"), + arrangehttp.ProvideServer("servers.alternate"), + + apiauth.Module, + oker.Module, + touchstone.Provide(), + touchhttp.Provide(), + metrics.Provide(), + ) + + if cli != nil && cli.Graph != "" { + _ = os.WriteFile(cli.Graph, []byte(g), 0600) + } + + if cli != nil && cli.Dev { + defer func() { + if gscfg != nil { + fmt.Fprintln(os.Stderr, gscfg.Explain().String()) + } + }() + } + + if err := app.Err(); err != nil { + return err + } + + if run { + app.Run() + } + + return nil +} + +// Provides a named type so it's a bit easier to flow through & use in fx. +type cliArgs []string + +// Handle the CLI processing and return the processed input. +func provideCLI(args cliArgs) (*CLI, error) { + return provideCLIWithOpts(args, false) +} + +func provideCLIWithOpts(args cliArgs, testOpts bool) (*CLI, error) { + var cli CLI + + // Create a no-op option to satisfy the kong.New() call. + var opt kong.Option = kong.OptionFunc( + func(*kong.Kong) error { + return nil + }, + ) + + if testOpts { + opt = kong.Writers(nil, nil) + } + + parser, err := kong.New(&cli, + kong.Name(applicationName), + kong.Description("The cpe agent for Xmidt service.\n"+ + fmt.Sprintf("\tVersion: %s\n", version)+ + fmt.Sprintf("\tDate: %s\n", date)+ + fmt.Sprintf("\tCommit: %s\n", commit)+ + fmt.Sprintf("\tBuilt By: %s\n", builtBy), + ), + kong.UsageOnError(), + opt, + ) + if err != nil { + return nil, err + } + + if testOpts { + parser.Exit = func(_ int) { panic("exit") } + } + + _, err = parser.Parse(args) + if err != nil { + parser.FatalIfErrorf(err) + } + + return &cli, nil +}