From e25fd891985c355561ba8574bc4e7432dd126865 Mon Sep 17 00:00:00 2001 From: Roberto Hidalgo Date: Sat, 20 Apr 2024 14:31:06 -0600 Subject: [PATCH] POST /-/hello-world --- .editorconfig | 21 ++ .gitignore | 4 + .golangci.yml | 38 +++ .tool-versions | 1 + LICENSE.txt | 201 +++++++++++++++ go.mod | 100 ++++++++ go.sum | 341 ++++++++++++++++++++++++++ internal/config/config.go | 104 ++++++++ internal/config/config_test.go | 136 ++++++++++ internal/config/consul.go | 99 ++++++++ internal/config/file.go | 70 ++++++ internal/payload/context.go | 29 +++ internal/payload/payload.go | 34 +++ internal/sink/debug/debug.go | 73 ++++++ internal/sink/nomad/nomad.go | 118 +++++++++ internal/sink/sink.go | 69 ++++++ internal/sink/types/types.go | 32 +++ internal/source/consul/consul.go | 65 +++++ internal/source/consul/consul_test.go | 67 +++++ internal/source/consul/watch.go | 69 ++++++ internal/source/http/http.go | 128 ++++++++++ internal/source/http/http_test.go | 101 ++++++++ internal/source/http/payload.go | 92 +++++++ internal/source/http/payload_test.go | 166 +++++++++++++ internal/source/manager.go | 111 +++++++++ internal/source/manager_test.go | 188 ++++++++++++++ internal/source/types/types.go | 33 +++ internal/source/types/types_test.go | 62 +++++ internal/version/version.go | 5 + main.go | 139 +++++++++++ 30 files changed, 2696 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .tool-versions create mode 100644 LICENSE.txt create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/config/consul.go create mode 100644 internal/config/file.go create mode 100644 internal/payload/context.go create mode 100644 internal/payload/payload.go create mode 100644 internal/sink/debug/debug.go create mode 100644 internal/sink/nomad/nomad.go create mode 100644 internal/sink/sink.go create mode 100644 internal/sink/types/types.go create mode 100644 internal/source/consul/consul.go create mode 100644 internal/source/consul/consul_test.go create mode 100644 internal/source/consul/watch.go create mode 100644 internal/source/http/http.go create mode 100644 internal/source/http/http_test.go create mode 100644 internal/source/http/payload.go create mode 100644 internal/source/http/payload_test.go create mode 100644 internal/source/manager.go create mode 100644 internal/source/manager_test.go create mode 100644 internal/source/types/types.go create mode 100644 internal/source/types/types_test.go create mode 100644 internal/version/version.go create mode 100644 main.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ebec1f5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 +max_line_length = 120 + +[*.go] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 +max_line_length = 120 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8b284c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +listeners.json +.vscode/* +coverage.* +event-gateway diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..b948379 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,38 @@ +run: + tests: false +linters-settings: + gocyclo: + min-complexity: 18 + tagliatelle: + case: + rules: + yaml: kebab + +linters: + fast: false + enable: + - errcheck + - exportloopref + - goconst + - gocritic + - gocyclo + - godot + - gofmt + - goimports + - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - nilerr + - prealloc + - revive + - staticcheck + - stylecheck + - tagliatelle + - typecheck + - unconvert + - unparam + - unused + - whitespace diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..e4ab54c --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +golang 1.21.1 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f1937c3 --- /dev/null +++ b/go.mod @@ -0,0 +1,100 @@ +module git.rob.mx/nidito/event-gateway + +go 1.21 + +require ( + git.rob.mx/nidito/chinampa v0.2.1 + github.com/cenkalti/backoff v2.2.1+incompatible + github.com/cenkalti/backoff/v4 v4.2.1 + github.com/hashicorp/consul/api v1.21.0 + github.com/hashicorp/nomad/api v0.0.0-20240418183417-ea5f2f6748c7 + github.com/honeycombio/honeycomb-opentelemetry-go v0.8.1 + github.com/honeycombio/otel-config-go v1.12.1 + github.com/julienschmidt/httprouter v1.3.0 + github.com/sirupsen/logrus v1.9.3 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 + go.opentelemetry.io/otel v1.18.0 + go.opentelemetry.io/otel/trace v1.18.0 +) + +require ( + github.com/alecthomas/chroma/v2 v2.13.0 // indirect + github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/glamour v0.7.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.19.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/hashicorp/cronexpr v1.1.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v0.16.2 // indirect + github.com/hashicorp/go-immutable-radix v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/serf v0.10.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sethvargo/go-envconfig v0.9.0 // indirect + github.com/shirou/gopsutil/v3 v3.23.8 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yuin/goldmark v1.7.1 // indirect + github.com/yuin/goldmark-emoji v1.0.2 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/host v0.44.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.44.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.19.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.18.0 // indirect + go.opentelemetry.io/otel/metric v1.18.0 // indirect + go.opentelemetry.io/otel/sdk v1.18.0 // indirect + go.opentelemetry.io/otel/sdk/metric v0.41.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/grpc v1.58.1 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..01abc28 --- /dev/null +++ b/go.sum @@ -0,0 +1,341 @@ +git.rob.mx/nidito/chinampa v0.2.1 h1:DlXiu2j8aKNMb5Z2Vr291DwiCl19ikyGrVpJEQn5kIw= +git.rob.mx/nidito/chinampa v0.2.1/go.mod h1:5X0gMayjUVs6biK6UoNZjEEc1wDEod+0PZg3ZrGgGUo= +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= +github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= +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/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= +github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= +github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/consul/api v1.21.0 h1:WMR2JiyuaQWRAMFaOGiYfY4Q4HRpyYRe/oYQofjyduM= +github.com/hashicorp/consul/api v1.21.0/go.mod h1:f8zVJwBcLdr1IQnfdfszjUM0xzp31Zl3bpws3pL9uFM= +github.com/hashicorp/consul/sdk v0.13.1 h1:EygWVWWMczTzXGpO93awkHFzfUka6hLYJ0qhETd+6lY= +github.com/hashicorp/consul/sdk v0.13.1/go.mod h1:SW/mM4LbKfqmMvcFu8v+eiQQ7oitXEFeiBe9StxERb0= +github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= +github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/nomad/api v0.0.0-20240418183417-ea5f2f6748c7 h1:pjE59CS2C9Bg+Xby0ROrnZSSBWtKwx3Sf9gqsrvIFSA= +github.com/hashicorp/nomad/api v0.0.0-20240418183417-ea5f2f6748c7/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +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/honeycombio/honeycomb-opentelemetry-go v0.8.1 h1:AjSqcF0naYr8owGuiNyoyKdcMxc2eMd2dZxiJiOiTVY= +github.com/honeycombio/honeycomb-opentelemetry-go v0.8.1/go.mod h1:4hVgnPiGsP58aypz5xzowQrtKwPh1Q6fvj8UZ8I8cdU= +github.com/honeycombio/otel-config-go v1.12.1 h1:7PiKBjXStElCvM95lphJEShNJGA2Ep8wbucC6yuYzQw= +github.com/honeycombio/otel-config-go v1.12.1/go.mod h1:6L4w8t0ttG+jacDhjFAn7TnaKUm/uqdA7QWokJLW8DY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= +github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE= +github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= +github.com/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE= +github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shoenig/test v1.7.1 h1:UJcjSAI3aUKx52kfcfhblgyhZceouhvvs3OYdWgn+PY= +github.com/shoenig/test v1.7.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= +github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/detectors/aws/lambda v0.44.0 h1:s+4DQOUrFZwKOS2cOQf9RwVgg0ZtUSSsi17PdDOz96g= +go.opentelemetry.io/contrib/detectors/aws/lambda v0.44.0/go.mod h1:DtJoiPxV7q/w2hqGPZSOfjfIigK/tkgihpsQhQFDl/Q= +go.opentelemetry.io/contrib/instrumentation/host v0.44.0 h1:SNqDjPpQmwFYvDipyJJxDbU5zKNWiYSMii864ubzIuQ= +go.opentelemetry.io/contrib/instrumentation/host v0.44.0/go.mod h1:bZcqg3yy0riQLNkx8dJWV4J3tbfL+6LQ5lIbI+vmarE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48= +go.opentelemetry.io/contrib/instrumentation/runtime v0.44.0 h1:TXu20nL4yYfJlQeqG/D3Ia6b0p2HZmLfJto9hqJTQ/c= +go.opentelemetry.io/contrib/instrumentation/runtime v0.44.0/go.mod h1:tQ5gBnfjndV1su3+DiLuu6rnd9hBBzg4rkRILnjSNFg= +go.opentelemetry.io/contrib/propagators/b3 v1.19.0 h1:ulz44cpm6V5oAeg5Aw9HyqGFMS6XM7untlMEhD7YzzA= +go.opentelemetry.io/contrib/propagators/b3 v1.19.0/go.mod h1:OzCmE2IVS+asTI+odXQstRGVfXQ4bXv9nMBRK0nNyqQ= +go.opentelemetry.io/contrib/propagators/ot v1.19.0 h1:vODRLMlKN4ApM8ri0UDk8nnEeISuwxpf67sE7PmOHhE= +go.opentelemetry.io/contrib/propagators/ot v1.19.0/go.mod h1:S2Uc7th2ZmLiHu0lrCmDCgTQ/y5Nbbis+TNjR1jjm4Q= +go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= +go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.41.0 h1:k0k7hFNDd8K4iOMJXj7s8sHaC4mhTlAeppRmZXLgZ6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.41.0/go.mod h1:hG4Fj/y8TR/tlEDREo8tWstl9fO9gcFkn4xrx0Io8xU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.41.0 h1:HgbDTD8pioFdY3NRc/YCvsWjqQPtweGyXxa32LgnTOw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.41.0/go.mod h1:tmvt/yK5Es5d6lHYWerLSOna8lCEfrBVX/a9M0ggqss= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.41.0 h1:iV3BOgW4fry1Riw9dwypigqlIYWXvSRVT2RJmblzo40= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.41.0/go.mod h1:7PGzqlKrxIRmbj5tlNW0nTkYZ5fHXDgk6Fy8/KjR0CI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0 h1:yE32ay7mJG2leczfREEhoW3VfSZIvHaB+gvVo1o8DQ8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0/go.mod h1:G17FHPDLt74bCI7tJ4CMitEk4BXTYG4FW6XUpkPBXa4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 h1:6pu8ttx76BxHf+xz/H77AUZkPF3cwWzXqAUsXhVKI18= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0/go.mod h1:IOmXxPrxoxFMXdNy7lfDmE8MzE61YPcurbUm0SMjerI= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.18.0 h1:hSWWvDjXHVLq9DkmB+77fl8v7+t+yYiS+eNkiplDK54= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.18.0/go.mod h1:zG7KQql1WjZCaUJd+L/ReSYx4bjbYJxg5ws9ws+mYes= +go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= +go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= +go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY= +go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M= +go.opentelemetry.io/otel/sdk/metric v0.41.0 h1:c3sAt9/pQ5fSIUfl0gPtClV3HhE18DCVzByD33R/zsk= +go.opentelemetry.io/otel/sdk/metric v0.41.0/go.mod h1:PmOmSt+iOklKtIg5O4Vz9H/ttcRFSNTgii+E1KGyn1w= +go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= +go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/grpc v1.58.1 h1:OL+Vz23DTtrrldqHK49FUOPHyY75rvFqJfXC84NYW58= +google.golang.org/grpc v1.58.1/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..063e9e9 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,104 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + "git.rob.mx/nidito/event-gateway/internal/sink" + "git.rob.mx/nidito/event-gateway/internal/sink/types" + source "git.rob.mx/nidito/event-gateway/internal/source/types" +) + +// RawSource is an intermediate source representation. +type RawSource struct { + Kind source.Kind `json:"kind"` + Config json.RawMessage +} + +func (s *RawSource) UnmarshalJSON(raw []byte) error { + s.Config = raw + inner := &struct { + Kind source.Kind `json:"kind"` + }{} + + if err := json.Unmarshal(raw, &inner); err != nil { + return fmt.Errorf("unable to decode source: %s: %s", raw, err) + } + + s.Kind = inner.Kind + return nil +} + +// Source is an interface concrete. +type Source interface { + Initialize() + Kind() source.Kind + Register(listener *Listener) error + Deregister(ID string) +} + +// Listener is the configuration for a source. +type Listener struct { + ID string + Source *RawSource + Event types.Event `json:"sink"` + Hash string +} + +func (l *Listener) UnmarshalJSON(raw []byte) error { + cfg := &struct { + Event json.RawMessage `json:"sink"` + Source *RawSource `json:"source"` + }{} + + if err := json.Unmarshal(raw, &cfg); err != nil { + return fmt.Errorf("unable to decode %s: %s", raw, err) + } + + l.Source = cfg.Source + + if cfg.Event == nil { + return fmt.Errorf("sink configuration not provided: %s", raw) + } + + sink, err := sink.Parse(cfg.Event) + if err != nil { + return fmt.Errorf("sink configuration for %s is invalid: %+v", raw, err) + } + l.Event = sink + + hasher := sha256.New() + hasher.Write(raw) + l.Hash = base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + + return nil +} + +// Loader represents a configurator interface. +type Loader interface { + Load() ([]*Listener, error) + String() string + Watch(chan []*Listener) +} + +// FromURN returns a Loader given a :// path. +func FromURN(urn string) (Loader, error) { + URLparts := strings.SplitN(urn, "://", 2) + if len(URLparts) == 1 { + URLparts = []string{"file", URLparts[0]} + } + scheme := URLparts[0] + configAddr := URLparts[1] + switch scheme { + case "file": + return &File{Path: configAddr}, nil + case "consul": + return NewConsul(configAddr) + } + return nil, fmt.Errorf("unknown config address: %s", urn) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..bd29764 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,136 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package config_test + +import ( + "strings" + "testing" + "testing/fstest" + + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/sink/debug" + "git.rob.mx/nidito/event-gateway/internal/source/types" +) + +func TestFileLoader(t *testing.T) { + + config.FS = fstest.MapFS{ + "empty-file.json": &fstest.MapFile{ + Data: []byte(`{}`), + }, + "bad-source.json": &fstest.MapFile{ + Data: []byte(`{ + "bad": { + "source": 42, + "sink": { + "kind": "debug" + } + } + }`), + }, + "no-sink.json": &fstest.MapFile{ + Data: []byte(`{ + "bad": { + "source": {"kind": "http"} + } + }`), + }, + "bad-sink.json": &fstest.MapFile{ + Data: []byte(`{ + "bad": { + "source": {"kind": "http"}, + "sink": 42 + } + }`), + }, + "simple.json": &fstest.MapFile{ + Data: []byte(`{ + "simple": { + "source": { + "kind": "http", + "path": "simple" + }, + "sink": { + "kind": "debug" + } + } + }`), + }, + } + + cases := []struct { + Name string + Path string + Error any + Expected []*config.Listener + }{ + { + Name: "empty-file", + Path: "empty-file.json", + Expected: []*config.Listener{}, + }, + { + Name: "bad-source", + Path: "bad-source.json", + Error: "could not unserialize bad-source.json as json: unable to decode {", + }, + { + Name: "no-sink", + Path: "no-sink.json", + Error: "could not unserialize no-sink.json as json: sink configuration not provided", + }, + { + Name: "bad-sink", + Path: "bad-sink.json", + Error: "could not unserialize bad-sink.json as json: sink configuration for {", + }, + { + Name: "simple", + Path: "simple.json", + Expected: []*config.Listener{ + { + ID: "simple", + Source: &config.RawSource{Kind: types.HTTP}, + Event: &debug.Event{}, + Hash: "8SsF8PtGcr-5xE5iy8F1JES0ZoTQMdfmbw7iLLU3wik=", + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + l := &config.File{Path: c.Path} + + res, err := l.Load() + if err != nil { + if c.Error != nil { + if errPrefix, ok := c.Error.(string); ok { + if !strings.HasPrefix(err.Error(), errPrefix) { + t.Fatalf("Unexpected error prefix, wanted %s, got %s", errPrefix, err) + } + } else if c.Error != err { + t.Fatalf("Unexpected error, wanted %s, got %s", c.Error, err) + } + + return + } + } + + if len(res) != len(c.Expected) { + t.Fatalf("Unexpected results, wanted %+v, got %+v", c.Expected, res) + } + + for idx, l := range c.Expected { + m := res[idx] + if l.ID != m.ID { + t.Fatalf("Unexpected ID, wanted %s got %s", l.ID, m.ID) + } + + if l.Hash != m.Hash { + t.Fatalf("Unexpected hash, wanted %s got %s", l.Hash, m.Hash) + } + } + }) + } +} diff --git a/internal/config/consul.go b/internal/config/consul.go new file mode 100644 index 0000000..eb48763 --- /dev/null +++ b/internal/config/consul.go @@ -0,0 +1,99 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + // "fmt" + // "os" + // "os/signal" + // "syscall" + + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "git.rob.mx/nidito/chinampa/pkg/logger" + "github.com/cenkalti/backoff" + "github.com/hashicorp/consul/api" +) + +var clogger = logger.Sub("event-gateway.config.consul") + +type Consul struct { + Prefix string + client *api.Client + lastIndex uint64 + cancel context.CancelFunc + registry chan []*Listener +} + +func NewConsul(prefix string) (Loader, error) { + cfg := api.DefaultConfig() + client, err := api.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("could not initialize Consul configuration client loader: %s", err) + } + + return &Consul{Prefix: prefix, client: client}, nil +} + +var _ Loader = &Consul{} + +func (c *Consul) String() string { + return fmt.Sprintf("ConsulLoader at %s", c.Prefix) +} + +func (c *Consul) Load() (res []*Listener, err error) { + opts := &api.QueryOptions{WaitIndex: c.lastIndex, WaitTime: 10 * time.Minute} + clogger.Debugf("Querying kv at %s", c.Prefix) + pairs, meta, err := c.client.KV().List(c.Prefix, opts) + if err != nil { + return res, fmt.Errorf("failed querying consul for config: %s", err) + } + clogger.Debugf("Querying ok: %+v, %d items found", meta.LastIndex, len(pairs)) + + c.lastIndex = meta.LastIndex + for _, p := range pairs { + l := &Listener{ID: strings.TrimPrefix(p.Key, c.Prefix+"/")} + clogger.Tracef("decoding config for %s", l.ID) + if err := json.Unmarshal(p.Value, l); err != nil { + return res, fmt.Errorf("could not decode config for %s/%s:%s", c.Prefix, p.Key, err) + } + clogger.Tracef("decoded config for %s", l.ID) + res = append(res, l) + } + return res, nil +} + +func (c *Consul) fetch() error { + clogger.Info("Watch expired, reloading consul configuration") + res, err := c.Load() + if err != nil { + return err + } + clogger.Info("Reloaded consul configuration") + c.registry <- res + return nil +} + +func (c *Consul) Watch(reg chan []*Listener) { + clogger.Infof("Starting config watch for %s", c.Prefix) + bgctx, cancel := context.WithCancel(context.Background()) + c.cancel = cancel + c.registry = reg + ctx := backoff.WithContext(backoff.NewExponentialBackOff(), bgctx) + + onUpdateError := func(err error, cooldown time.Duration) { + clogger.Errorf("%s. Retrying in %vs", err, cooldown.Truncate(time.Second)) + } + + go func() { + for { + if err := backoff.RetryNotify(c.fetch, ctx, onUpdateError); err != nil { + continue + } + } + }() +} diff --git a/internal/config/file.go b/internal/config/file.go new file mode 100644 index 0000000..6740141 --- /dev/null +++ b/internal/config/file.go @@ -0,0 +1,70 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "os/signal" + "syscall" + + "git.rob.mx/nidito/chinampa/pkg/logger" +) + +var flog = logger.Sub("event-gateway.config.file") + +var FS fs.FS + +type fileSchema map[string]*Listener + +type File struct { + Path string +} + +var _ Loader = &File{} + +func (l *File) String() string { + return fmt.Sprintf("FileLoader at %s", l.Path) +} + +func (l *File) Load() (res []*Listener, err error) { + var data []byte + if FS != nil { + data, err = fs.ReadFile(FS, l.Path) + } else { + data, err = os.ReadFile(l.Path) + } + if err != nil { + return res, fmt.Errorf("could not read %s: %s", l.Path, err) + } + + config := &fileSchema{} + if err := json.Unmarshal(data, &config); err != nil { + return res, fmt.Errorf("could not unserialize %s as json: %s", l.Path, err) + } + + for name, listener := range *config { + listener.ID = name + res = append(res, listener) + } + return res, nil +} + +func (l *File) Watch(reg chan []*Listener) { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGHUP) + go func() { + for { + <-c + flog.Info("SIGHUP detected, reloading file configuration") + res, err := l.Load() + if err != nil { + flog.Errorf("could not reload file configuration: %s", err) + } + flog.Info("Reloaded file configuration, updating listeners") + reg <- res + } + }() +} diff --git a/internal/payload/context.go b/internal/payload/context.go new file mode 100644 index 0000000..f084d5a --- /dev/null +++ b/internal/payload/context.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package payload + +import ( + "context" + "fmt" +) + +type contextKey string + +const ContextKey contextKey = "x-event-payload" + +func FromContext(ctx context.Context) (*Payload, error) { + pf := ctx.Value(ContextKey) + if pf == nil { + return nil, fmt.Errorf("no payload found in context: %+v", ctx) + } + + if parser, ok := pf.(Parser); ok { + return parser.Parse() + } + + return nil, fmt.Errorf("value at %s is not a payload.Parser, it's a %+v", ContextKey, pf) +} + +func Context(ctx context.Context, parser Parser) context.Context { + return context.WithValue(ctx, ContextKey, parser) +} diff --git a/internal/payload/payload.go b/internal/payload/payload.go new file mode 100644 index 0000000..61a6647 --- /dev/null +++ b/internal/payload/payload.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package payload + +import ( + "encoding/base64" + "encoding/json" +) + +type Parser interface { + Parse() (*Payload, error) +} + +type Kind string + +const ( + KindText Kind = "text" + KindJSON Kind = "json" + KindForm Kind = "form" +) + +type Payload struct { + Kind Kind `json:"type"` + Value any `json:"value"` +} + +func (p *Payload) Encoded() ([]byte, error) { + b, err := json.Marshal(map[string]any{"type": p.Kind, "value": p.Value}) + if err != nil { + return nil, err + } + + return []byte(base64.StdEncoding.EncodeToString(b)), nil +} diff --git a/internal/sink/debug/debug.go b/internal/sink/debug/debug.go new file mode 100644 index 0000000..ebd47bb --- /dev/null +++ b/internal/sink/debug/debug.go @@ -0,0 +1,73 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package debug + +import ( + "context" + + "git.rob.mx/nidito/chinampa/pkg/logger" + "git.rob.mx/nidito/event-gateway/internal/payload" + "git.rob.mx/nidito/event-gateway/internal/sink/types" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +var log = logger.Sub("event-gateway.sink.debug") + +var MaxDebugCalls = 10 + +type Event struct{} + +func (e *Event) String() string { + return "debug" +} +func (e *Event) Kind() types.Kind { + return types.Debug +} + +type Sink struct { + Calls []context.Context + tracer trace.Tracer +} + +func New() (types.Sink, error) { + return &Sink{ + Calls: []context.Context{}, + tracer: otel.Tracer("event-gateway.sink.debug"), + }, nil +} + +func (s *Sink) Tracer() trace.Tracer { + return s.tracer +} + +func (s *Sink) String() string { + return "debug" +} + +func (s *Sink) Parse(c *types.Config) (types.Event, error) { + return &Event{}, nil +} + +func (s *Sink) Dispatch(ctx context.Context, span trace.Span, event types.Event) error { + log.Infof("Dispatching handler for: %+v", event) + s.Calls = append(s.Calls, ctx) + if len(s.Calls) > MaxDebugCalls { + // keep the most recent in memory, up to MaxDebugCalls + s.Calls = s.Calls[len(s.Calls)-MaxDebugCalls : len(s.Calls)] + } + + data, err := payload.FromContext(ctx) + if err != nil { + span.RecordError(err) + return err + } + + var val any + if data != nil && data.Value != nil { + val = data.Value + } + + log.Infof("Debug handler dispatched: %+v", val) + return nil +} diff --git a/internal/sink/nomad/nomad.go b/internal/sink/nomad/nomad.go new file mode 100644 index 0000000..f607804 --- /dev/null +++ b/internal/sink/nomad/nomad.go @@ -0,0 +1,118 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package nomad + +import ( + "context" + "encoding/json" + "fmt" + + "git.rob.mx/nidito/chinampa/pkg/logger" + "git.rob.mx/nidito/event-gateway/internal/payload" + "git.rob.mx/nidito/event-gateway/internal/sink/types" + "github.com/hashicorp/nomad/api" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +var log = logger.Sub("event-gateway.sink.nomad") + +type Job struct { + Job string `json:"job"` + Namespace string `json:"namespace"` + Region string `json:"region"` + Payload bool `json:"payload"` +} + +var _ types.Event = &Job{} + +func (j *Job) Kind() types.Kind { + return types.Nomad +} + +type Sink struct { + Client *api.Client + tracer trace.Tracer +} + +var _ types.Sink = &Sink{} + +func New() (types.Sink, error) { + cfg := api.DefaultConfig() + c, err := api.NewClient(cfg) + if err != nil { + return nil, err + } + return &Sink{ + Client: c, + tracer: otel.Tracer("event-gateway.sink.nomad"), + }, nil +} + +func (s *Sink) Parse(c *types.Config) (types.Event, error) { + target := &Job{} + if err := json.Unmarshal(c.Data, &target); err != nil { + return nil, fmt.Errorf("failed to parse nomad handler: %s", err) + } + + return target, nil +} + +func (s *Sink) Tracer() trace.Tracer { + return s.tracer +} + +func (s *Sink) Dispatch(ctx context.Context, span trace.Span, event types.Event) error { + j := event.(*Job) + span.SetAttributes( + attribute.KeyValue{ + Key: "job", + Value: attribute.StringValue(j.Job), + }, + attribute.KeyValue{ + Key: "namespace", + Value: attribute.StringValue(j.Namespace), + }, + ) + defer span.End() + + var p []byte + + if j.Payload { + data, err := payload.FromContext(ctx) + if err != nil { + return err + } + + serialized, err := data.Encoded() + if err != nil { + return err + } + p = serialized + } + opts := &api.WriteOptions{} + + if j.Namespace != "" { + opts.Namespace = j.Namespace + } + + if j.Region != "" { + opts.Region = j.Region + } + + meta := map[string]string{} + log.Infof("dispatching nomad job %s", j.Job) + span.AddEvent("nomad.call") + res, _, err := s.Client.Jobs().Dispatch(j.Job, meta, p, "", opts) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "nomad.dispatch.fail") + return fmt.Errorf("could not dispatch nomad job: %s", err) + } + + span.SetStatus(codes.Ok, "") + log.Infof("nomad job dispatched: %s (%s)", j.Job, res.DispatchedJobID) + return nil +} diff --git a/internal/sink/sink.go b/internal/sink/sink.go new file mode 100644 index 0000000..d5023ed --- /dev/null +++ b/internal/sink/sink.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package sink + +import ( + "context" + "encoding/json" + "fmt" + + "git.rob.mx/nidito/event-gateway/internal/sink/debug" + "git.rob.mx/nidito/event-gateway/internal/sink/nomad" + "git.rob.mx/nidito/event-gateway/internal/sink/types" +) + +type registry map[types.Kind]types.Sink + +var sinks = registry{} + +func Clear() { + sinks = registry{} +} + +func (r registry) ForKind(k types.Kind) (types.Sink, error) { + if _, ok := r[k]; !ok { + switch k { + case types.Nomad: + s, err := nomad.New() + if err != nil { + return nil, err + } + r[k] = s + case types.Debug: + s, err := debug.New() + if err != nil { + return nil, err + } + r[k] = s + } + } + return r[k], nil +} + +func ForKind(k types.Kind) (types.Sink, error) { + return sinks.ForKind(k) +} + +func Parse(config json.RawMessage) (types.Event, error) { + hc := &types.Config{Data: config} + if err := json.Unmarshal(config, &hc); err != nil { + return nil, err + } + + sink, err := sinks.ForKind(hc.Kind) + if err != nil { + return nil, err + } + + return sink.Parse(hc) +} + +func Dispatch(ctx context.Context, event types.Event) error { + sink, err := sinks.ForKind(event.Kind()) + if err != nil { + return err + } + _, span := sink.Tracer().Start(ctx, fmt.Sprintf("dispatch.%s", event.Kind())) + defer span.End() + return sink.Dispatch(ctx, span, event) +} diff --git a/internal/sink/types/types.go b/internal/sink/types/types.go new file mode 100644 index 0000000..879ea6f --- /dev/null +++ b/internal/sink/types/types.go @@ -0,0 +1,32 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package types + +import ( + "context" + "encoding/json" + + "go.opentelemetry.io/otel/trace" +) + +type Kind string + +const ( + Nomad Kind = "nomad" + Debug Kind = "debug" +) + +type Sink interface { + Dispatch(ctx context.Context, span trace.Span, event Event) error + Tracer() trace.Tracer + Parse(c *Config) (Event, error) +} + +type Event interface { + Kind() Kind +} + +type Config struct { + Kind Kind `json:"kind"` + Data json.RawMessage +} diff --git a/internal/source/consul/consul.go b/internal/source/consul/consul.go new file mode 100644 index 0000000..78992ed --- /dev/null +++ b/internal/source/consul/consul.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package consul + +import ( + "encoding/json" + + "git.rob.mx/nidito/chinampa/pkg/logger" + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/source/types" + "github.com/hashicorp/consul/api" +) + +var log = logger.Sub("event-gateway.source.http") + +type Client interface { + List(name string, q *api.QueryOptions) ([]*api.UserEvent, *api.QueryMeta, error) +} + +type Source struct { + watches map[string]*watch + Client Client +} + +var _ config.Source = &Source{} + +func New(client Client) *Source { + s := &Source{Client: client} + s.Initialize() + return s +} + +func (src *Source) Kind() types.Kind { + return types.Consul +} + +func (src *Source) Initialize() { + if src.watches == nil { + src.watches = map[string]*watch{} + } +} + +func (src *Source) Register(l *config.Listener) error { + listener := &watch{Name: l.ID, Event: l.Event} + if err := json.Unmarshal(l.Source.Config, &listener); err != nil { + return err + } + + if l, ok := src.watches[listener.Name]; ok { + listener.lastIndex = l.lastIndex + listener.cancel = l.cancel + } + + src.watches[listener.Name] = listener + listener.Watch(src.Client) + return nil +} + +func (src *Source) Deregister(id string) { + if watch, ok := src.watches[id]; ok { + log.Debugf("Clearing watch for consul:%s", watch.Name) + watch.cancel() + delete(src.watches, id) + } +} diff --git a/internal/source/consul/consul_test.go b/internal/source/consul/consul_test.go new file mode 100644 index 0000000..7fa11a1 --- /dev/null +++ b/internal/source/consul/consul_test.go @@ -0,0 +1,67 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package consul_test + +import ( + "testing" + "time" + + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/sink/debug" + "git.rob.mx/nidito/event-gateway/internal/source/consul" + "git.rob.mx/nidito/event-gateway/internal/source/types" + "github.com/hashicorp/consul/api" + "github.com/sirupsen/logrus" +) + +type mockConsul struct { + calls []string +} + +func (mc *mockConsul) List(name string, q *api.QueryOptions) ([]*api.UserEvent, *api.QueryMeta, error) { + time.Sleep(10 * time.Millisecond) + mc.calls = append(mc.calls, name) + return []*api.UserEvent{}, &api.QueryMeta{LastIndex: 100}, nil +} + +func TestConsulReloads(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + client := &mockConsul{} + + c := &consul.Source{Client: client} + c.Initialize() + + if len(client.calls) != 0 { + t.Fatalf("empty consul fetched events: %+v", client.calls) + } + + target := &debug.Event{} + l := &config.Listener{ + ID: "some-event-name", + Source: &config.RawSource{ + Kind: types.Consul, + Config: []byte(`{}`), + }, + Event: target, + } + + if err := c.Register(l); err != nil { + t.Fatalf("could not register source: %s", err) + } + time.Sleep(15 * time.Millisecond) + if len(client.calls) == 0 { + t.Fatalf("consul did not fetch events: %+v", client.calls) + } + + had := len(client.calls) + c.Deregister(l.ID) + + afterClear := append([]string{}, client.calls...) + if len(afterClear) > had { + t.Fatalf("consul continued fetching events (%d > %d): %+v", len(afterClear), had, afterClear) + } + + if len(afterClear) != len(client.calls) { + t.Fatalf("continued fetching after clear and run: %+v", client.calls) + } +} diff --git a/internal/source/consul/watch.go b/internal/source/consul/watch.go new file mode 100644 index 0000000..a33379b --- /dev/null +++ b/internal/source/consul/watch.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package consul + +import ( + "context" + "time" + + "git.rob.mx/nidito/event-gateway/internal/payload" + "git.rob.mx/nidito/event-gateway/internal/sink" + "git.rob.mx/nidito/event-gateway/internal/sink/types" + "github.com/cenkalti/backoff/v4" + "github.com/hashicorp/consul/api" +) + +type watch struct { + Name string + DC string `json:"dc"` + Filter *string `json:"filter,omitempty"` + Transform *string `json:"transform,omitempty"` + Event types.Event + lastIndex uint64 + cancel context.CancelFunc + client Client +} + +func (c *watch) Watch(client Client) { + log.Infof("Starting lookup for %s", c.Name) + bgctx, cancel := context.WithCancel(context.Background()) + c.cancel = cancel + ctx := backoff.WithContext(backoff.NewExponentialBackOff(), bgctx) + + onUpdateError := func(err error, cooldown time.Duration) { + log.Errorf("Could not lookup %s, retrying in %vs: %v", c.Name, cooldown.Truncate(time.Second), err) + } + c.client = client + + go func() { + for { + if err := backoff.RetryNotify(c.fetch, ctx, onUpdateError); err != nil { + continue + } + } + }() +} + +func (c *watch) fetch() error { + opts := &api.QueryOptions{ + WaitTime: 10 * time.Minute, + WaitIndex: c.lastIndex, + } + evts, meta, err := c.client.List(c.Name, opts) + if err != nil { + return err + } + + c.lastIndex = meta.LastIndex + + for _, evt := range evts { + log.Infof("Incoming event from source: consul:%s", c.Name) + ctx := context.WithValue(context.Background(), payload.ContextKey, evt.Payload) + // ctx = context.WithValue(ctx, "event", "consul:"+c.Name) + if err := sink.Dispatch(ctx, c.Event); err != nil { + log.Errorf("could not trigger sink from consul:%s %s\n", c.Name, err) + } + } + + return nil +} diff --git a/internal/source/http/http.go b/internal/source/http/http.go new file mode 100644 index 0000000..75432c2 --- /dev/null +++ b/internal/source/http/http.go @@ -0,0 +1,128 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package http + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "git.rob.mx/nidito/chinampa/pkg/logger" + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/payload" + "git.rob.mx/nidito/event-gateway/internal/sink" + "git.rob.mx/nidito/event-gateway/internal/sink/types" + types1 "git.rob.mx/nidito/event-gateway/internal/source/types" + "github.com/julienschmidt/httprouter" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +var log = logger.Sub("event-gateway.source.http") + +type listener struct { + ID string + Path string `json:"path"` + Event types.Event +} + +type Source struct { + Router *httprouter.Router + listeners map[string]*listener + byPath map[string]*listener +} + +var _ config.Source = &Source{} + +func New(router *httprouter.Router) *Source { + if router == nil { + router = httprouter.New() + } + + s := &Source{ + Router: router, + } + s.Initialize() + return s +} + +func (src *Source) Kind() types1.Kind { + return types1.HTTP +} + +func (src *Source) Initialize() { + src.listeners = map[string]*listener{} + src.byPath = map[string]*listener{} + handler := otelhttp.NewHandler(src, "http-event") + src.Router.Handler("GET", "/-/:source", handler) + src.Router.Handler("POST", "/-/:source", handler) +} + +func (src *Source) Register(l *config.Listener) error { + source := &listener{ID: l.ID, Event: l.Event} + if err := json.Unmarshal(l.Source.Config, &source); err != nil { + return err + } + + if source.Path == "" { + return fmt.Errorf("no path set for http source %s: %+v", l.ID, l.Source.Config) + } + + log.Debugf("http: registering %s (%s)", source.ID, source.Path) + src.listeners[source.ID] = source + src.byPath[source.Path] = source + return nil +} + +func (src *Source) Deregister(id string) { + if existing, ok := src.listeners[id]; ok { + log.Debugf("http: deregistering %s (%s)", id, existing.Path) + delete(src.byPath, existing.Path) + delete(src.listeners, id) + } +} + +func (src *Source) ServeHTTP(w http.ResponseWriter, r *http.Request) { + p := httprouter.ParamsFromContext(r.Context()) + var response string + sourceID := p.ByName("source") + source, ok := src.byPath[sourceID] + span := trace.SpanFromContext(r.Context()) + if ok { + span.SetAttributes(attribute.String("source", source.ID)) + log.Infof("Incoming event from source: http:%s", source.ID) + response = "processed" + + go func(r *http.Request) { + ctx := payload.Context(r.Context(), &Payload{Req: r.Clone(context.Background())}) + if err := sink.Dispatch(ctx, source.Event); err != nil { + log.Errorf("could not trigger source: %s\n", err) + span.RecordError(err) + span.SetStatus(codes.Error, "could not trigger source") + span.End() + return + } + span.SetStatus(codes.Ok, "") + }(r) + } else { + span.SetAttributes(attribute.String("source", sourceID)) + span.RecordError(fmt.Errorf("no listener for source")) + span.End() + response = "ignored" + log.Debugf("Ignoring unknown source: http:%s", sourceID) + } + + w.Write([]byte(response)) +} + +func (src *Source) Events() []string { + res := []string{} + for _, l := range src.listeners { + res = append(res, l.ID) + } + return res +} diff --git a/internal/source/http/http_test.go b/internal/source/http/http_test.go new file mode 100644 index 0000000..3c5adab --- /dev/null +++ b/internal/source/http/http_test.go @@ -0,0 +1,101 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package http_test + +import ( + "bytes" + "net/http/httptest" + "reflect" + "testing" + "time" + + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/payload" + "git.rob.mx/nidito/event-gateway/internal/sink" + "git.rob.mx/nidito/event-gateway/internal/sink/debug" + "git.rob.mx/nidito/event-gateway/internal/source/http" + "git.rob.mx/nidito/event-gateway/internal/source/types" + "github.com/julienschmidt/httprouter" + "github.com/sirupsen/logrus" +) + +func TestHTTPEmptyRouter(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + server := http.New(nil) + + evts := server.Events() + if len(evts) != 0 { + t.Fatalf("new http source has non-zero sources: %+v", evts) + } + + res := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/-/some-urlsafe-key", bytes.NewBufferString(`{"test": "test}`)) + server.ServeHTTP(res, req) + + body := res.Body.String() + if body != "ignored" { + t.Fatalf("unexpected response wanted ignored, got: %s", body) + } +} + +func TestHTTPRegistration(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + + router := httprouter.New() + el := http.New(router) + + target := &debug.Event{} + l := &config.Listener{ + ID: "test", + Source: &config.RawSource{ + Kind: types.HTTP, + Config: []byte(`{"path": "some-urlsafe-key"}`), + }, + Event: target, + } + if err := el.Register(l); err != nil { + t.Fatalf("failed to register good service") + } + + evts := el.Events() + expected := []string{"test"} + if !reflect.DeepEqual(evts, expected) { + t.Fatalf("unexpected source paths, wanted %+v, got %+v", expected, evts) + } + + res := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/-/some-urlsafe-key", bytes.NewBufferString(`{"test": "test"}`)) + req.Header.Add("content-type", "application/json") + router.ServeHTTP(res, req) + + body := res.Body.String() + if body != "processed" { + t.Fatalf("unexpected response wanted ok, got: %s", body) + } + + time.Sleep(150 * time.Millisecond) + + sI, err := sink.ForKind(target.Kind()) + if err != nil { + t.Fatalf("could not get debug sink %s", err) + } + s := sI.(*debug.Sink) + + if len(s.Calls) != 1 { + t.Fatalf("unexpected number of triggers called: %+v", s.Calls) + } + + payload, err := payload.FromContext(s.Calls[0]) + if err != nil { + t.Fatalf("Could not parse payload: %s", err) + } + + if payload == nil { + t.Fatalf("No payload parsed: %s fn: %+v", payload, s.Calls[0]) + } + + wanted := map[string]any{"test": "test"} + if !reflect.DeepEqual(payload.Value, wanted) { + t.Fatalf("unexpected payload, wanted %+v, got: %+v", wanted, payload.Value) + } +} diff --git a/internal/source/http/payload.go b/internal/source/http/payload.go new file mode 100644 index 0000000..8adce09 --- /dev/null +++ b/internal/source/http/payload.go @@ -0,0 +1,92 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package http + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "git.rob.mx/nidito/event-gateway/internal/payload" +) + +type Payload struct { + Req *http.Request + resolved *payload.Payload +} + +func (p *Payload) Parse() (*payload.Payload, error) { + if p.resolved == nil { + contentType := p.Req.Header.Get("Content-Type") + if contentType == "" { + contentType = "text/plain" + } else { + contentType = strings.SplitN(contentType, ";", 2)[0] + } + p.resolved = &payload.Payload{} + + switch contentType { + case "text/plain": + body, err := io.ReadAll(p.Req.Body) + if err != nil { + return p.resolved, fmt.Errorf("could not decode text/plain payload: %s", err) + } + if len(body) == 0 { + return nil, nil + } + + p.resolved.Kind = "text" + p.resolved.Value = string(body) + case "application/json": + body, err := io.ReadAll(p.Req.Body) + if err != nil { + return p.resolved, fmt.Errorf("could not read json body: %s", err) + } + var data any + if err := json.Unmarshal(body, &data); err != nil { + return p.resolved, fmt.Errorf("could not decode json payload: %s", err) + } + p.resolved.Kind = "json" + p.resolved.Value = data + case "application/x-www-form-urlencoded": + if err := p.Req.ParseForm(); err != nil { + return p.resolved, fmt.Errorf("could not decode %s payload: %s", contentType, err) + } + log.Debugf("parsed form: %+v", p.Req.Form) + + form := map[string]any{} + + for key, values := range p.Req.PostForm { + log.Debugf("parsing form: %s", key) + if len(values) > 1 { + form[key] = values + } else { + form[key] = p.Req.FormValue(key) + } + } + p.resolved.Kind = "form" + p.resolved.Value = form + case "multipart/form-data": + if err := p.Req.ParseMultipartForm(100); err != nil { + if body, err := io.ReadAll(p.Req.Body); err == nil { + log.Errorf("request %s body: %s", p.Req.Header["Content-Type"], body) + } + return p.resolved, fmt.Errorf("could not decode %s payload: %s", contentType, err) + } + + form := map[string]any{} + for key, values := range p.Req.MultipartForm.Value { + if len(values) > 1 { + form[key] = values + } else { + form[key] = p.Req.FormValue(key) + } + } + p.resolved.Kind = "form" + p.resolved.Value = form + } + } + return p.resolved, nil +} diff --git a/internal/source/http/payload_test.go b/internal/source/http/payload_test.go new file mode 100644 index 0000000..4fd5d6c --- /dev/null +++ b/internal/source/http/payload_test.go @@ -0,0 +1,166 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package http_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http/httptest" + "reflect" + "strings" + "testing" + "time" + + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/payload" + "git.rob.mx/nidito/event-gateway/internal/sink" + "git.rob.mx/nidito/event-gateway/internal/sink/debug" + "git.rob.mx/nidito/event-gateway/internal/source/http" + "git.rob.mx/nidito/event-gateway/internal/source/types" + "github.com/sirupsen/logrus" +) + +func TestHTTPPayload(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + + testCases := []struct { + Name string + Payload any + Expect any + ExpectKind payload.Kind + ContentType string + }{ + { + Name: "empty-payload", + Payload: nil, + Expect: nil, + ExpectKind: "", + }, + { + Name: "text-payload", + Payload: "sup", + Expect: "sup", + ExpectKind: payload.KindText, + }, + { + Name: "json-payload", + Payload: map[string]any{"yo": "sup"}, + ContentType: "application/json", + Expect: map[string]any{"yo": "sup"}, + ExpectKind: payload.KindJSON, + }, + { + Name: "form-urlencoded-payload", + Payload: "yo=sup&yo=quihubo&dude=sweet", + ContentType: "application/x-www-form-urlencoded", + Expect: map[string]any{"yo": []string{"sup", "quihubo"}, "dude": "sweet"}, + ExpectKind: payload.KindForm, + }, + { + Name: "form-data-payload", + Payload: strings.ReplaceAll(` +--424242 +Content-Disposition: form-data; name="yo" + +sup +--424242 +Content-Disposition: form-data; name="yo" + +quihubo +--424242 +Content-Disposition: form-data; name="dude" + +sweet +--424242 +Content-Disposition: form-data; name="ignored"; filename="example.txt" +Content-type: text/plain + +some ignored text +--424242-- +`, "\n", "\r\n"), + ContentType: `multipart/form-data; boundary=424242`, + Expect: map[string]any{"yo": []string{"sup", "quihubo"}, "dude": "sweet"}, + ExpectKind: payload.KindForm, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + sink.Clear() + server := http.New(nil) + + target := &debug.Event{} + l := &config.Listener{ + ID: tc.Name, + Source: &config.RawSource{ + Kind: types.HTTP, + Config: []byte(fmt.Sprintf(`{"path": "%s"}`, tc.Name)), + }, + Event: target, + } + if err := server.Register(l); err != nil { + t.Fatalf("failed to register %s", tc.Name) + } + + outbound := &bytes.Buffer{} + if tc.Payload != nil { + if tc.ContentType == "application/json" { + if serialized, err := json.Marshal(tc.Payload); err != nil { + t.Fatalf("could not serialize %s payload: %s", tc.Name, err) + } else { + outbound = bytes.NewBuffer(serialized) + } + } else { + outbound = bytes.NewBufferString(tc.Payload.(string)) + } + } + + res := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/-/"+tc.Name, outbound) + + if tc.ContentType != "" { + req.Header["Content-Type"] = []string{tc.ContentType} + } + + server.Router.ServeHTTP(res, req) + defer req.Body.Close() + + if res.Code > 200 { + body, _ := io.ReadAll(res.Body) + logrus.Errorf("request headers: %+v", req.Header) + t.Fatalf("Request ended with non 200 code: %d: %s", res.Code, body) + } + + time.Sleep(15 * time.Millisecond) + + sI, err := sink.ForKind(target.Kind()) + if err != nil { + t.Fatalf("could not get debug sink %s", err) + } + s := sI.(*debug.Sink) + if len(s.Calls) != 1 { + t.Fatalf("unexpected number of triggers called: %+v", s.Calls) + } + + payload, err := payload.FromContext(s.Calls[0]) + if err != nil { + t.Fatalf("Could not parse payload: %s", err) + } + + var got any = nil + if tc.Expect != nil { + if payload == nil { + t.Fatalf("no payload parsed!") + } + got = payload.Value + } + + if !reflect.DeepEqual(got, tc.Expect) { + t.Fatalf("%s: unexpected payload, wanted %v, got: %v", tc.Name, tc.Expect, got) + } + }) + } + +} diff --git a/internal/source/manager.go b/internal/source/manager.go new file mode 100644 index 0000000..7a73376 --- /dev/null +++ b/internal/source/manager.go @@ -0,0 +1,111 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package source + +import ( + "fmt" + + "git.rob.mx/nidito/chinampa/pkg/logger" + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/source/types" +) + +var log = logger.Sub("event-gateway.manager") + +type Manager struct { + Sources map[types.Kind]config.Source + Config config.Loader + Ready bool + listeners map[string]*config.Listener +} + +func (m *Manager) AddSource(src config.Source) { + if m.Sources == nil { + m.Sources = map[types.Kind]config.Source{} + } + m.Sources[src.Kind()] = src +} + +func (m *Manager) Listen(cfg config.Loader) error { + m.Config = cfg + for _, src := range m.Sources { + src.Initialize() + } + + if err := m.watchConfig(); err != nil { + return fmt.Errorf("could not initialize config loading: %s", err) + } + + return nil +} + +func (m *Manager) watchConfig() error { + updates := make(chan []*config.Listener, 1) + go func() { + for { + listeners := <-updates + log.Debugf("manager got updated listeners with count %d", len(listeners)) + if err := m.onUpdate(listeners); err != nil { + log.Errorf("could not process updated listeners: %s", err) + } + } + }() + + log.Debugf("Loading configuration with %s", m.Config) + res, err := m.Config.Load() + if err != nil { + return fmt.Errorf("error loading config using %s: %s", m.Config, err) + } + log.Debugf("Loaded configuration with %s, starting watch", m.Config) + + updates <- res + m.Config.Watch(updates) + + return nil +} + +func (m *Manager) onUpdate(listeners []*config.Listener) error { + updated := map[string]*config.Listener{} + for _, l := range listeners { + src, srcExists := m.Sources[l.Source.Kind] + if !srcExists { + return fmt.Errorf("unknown source kind <%s> for %s: %+v", l.Source.Kind, l.ID, l) + } + + if l.Event == nil { + return fmt.Errorf("sink configuration for %s not provided: %+v", l.ID, l) + } + + action := "registered" + if existing, exists := m.listeners[l.ID]; exists { + if l.Hash == existing.Hash { + log.Infof("Same configuration for %s, will not reload", l.ID) + updated[l.ID] = l + continue + } + log.Infof("Reloading configuration for %s", l.ID) + src.Deregister(l.ID) + action = "reloaded" + } else { + log.Debugf("registering %s with source %s", l.ID, l.Source.Kind) + } + + if err := src.Register(l); err != nil { + log.Errorf("could not register %s with source %s: %s", l.Source.Kind, l.ID, err) + } + log.Infof("%s %s in source %s", action, l.ID, l.Source.Kind) + updated[l.ID] = l + } + + for id, l := range m.listeners { + if _, exists := updated[id]; !exists { + log.Infof("Deleting listener for %s as it's no longer in configuration", l.ID) + m.Sources[l.Source.Kind].Deregister(id) + } + } + + m.listeners = updated + m.Ready = true + + return nil +} diff --git a/internal/source/manager_test.go b/internal/source/manager_test.go new file mode 100644 index 0000000..3b9a461 --- /dev/null +++ b/internal/source/manager_test.go @@ -0,0 +1,188 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package source_test + +import ( + "bytes" + "net/http/httptest" + "syscall" + "testing" + "testing/fstest" + "time" + + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/source" + "git.rob.mx/nidito/event-gateway/internal/source/http" + "github.com/julienschmidt/httprouter" + "github.com/sirupsen/logrus" +) + +func TestManagerLifecycle(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + + payload := bytes.NewBufferString(`{"test": "test"}`) + headers := map[string]string{ + "content-type": "application/json", + } + + router := httprouter.New() + src := &http.Source{Router: router} + manager := &source.Manager{} + manager.AddSource(src) + + cfg, err := config.FromURN("file://config.json") + if err != nil { + t.Fatalf("could not initialize configuration loader: %s", err) + } + + // config loads + t.Run("config loads", func(t *testing.T) { + config.FS = fstest.MapFS{ + "config.json": &fstest.MapFile{ + Data: []byte(`{ + "event-id": { + "source": { + "kind": "http", + "path": "some-path" + }, + "sink": { + "kind": "debug" + } + } + }`), + }, + } + + if err := manager.Listen(cfg); err != nil { + t.Fatalf("could not start listener: %s", err) + } + + time.Sleep(10 * time.Millisecond) + res := testEvent("/-/some-path", payload, headers, router) + body := res.Body.String() + if body != "processed" { + t.Fatalf("unexpected response wanted ok, got: %s", body) + } + }) + + t.Run("config updates and deletes", func(t *testing.T) { + config.FS = &fstest.MapFS{ + "config.json": &fstest.MapFile{ + Data: []byte(`{}`), + }, + } + + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + time.Sleep(10 * time.Millisecond) + + res := testEvent("/-/some-path", payload, headers, router) + + body := res.Body.String() + if body != "ignored" { + t.Fatalf("unexpected response wanted ignored, got: %s", body) + } + }) + + t.Run("config updates and re-adds", func(t *testing.T) { + config.FS = &fstest.MapFS{ + "config.json": &fstest.MapFile{ + Data: []byte(`{ + "event-id": { + "source": { + "kind": "http", + "path": "some-path" + }, + "sink": { + "kind": "debug" + } + } + }`), + }, + } + + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + time.Sleep(10 * time.Millisecond) + + res := testEvent("/-/some-path", payload, headers, router) + body := res.Body.String() + if body != "processed" { + t.Fatalf("unexpected response wanted processed, got: %s", body) + } + }) + + t.Run("config replaces", func(t *testing.T) { + config.FS = &fstest.MapFS{ + "config.json": &fstest.MapFile{ + Data: []byte(`{ + "replaced": { + "source": { + "kind": "http", + "path": "some-other-path" + }, + "sink": { + "kind": "debug" + } + } + }`), + }, + } + + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + time.Sleep(10 * time.Millisecond) + + res := testEvent("/-/some-other-path", payload, headers, router) + body := res.Body.String() + if body != "processed" { + t.Fatalf("unexpected response wanted processed, got: %s", body) + } + }) + + t.Run("config updates listener with changes", func(t *testing.T) { + config.FS = &fstest.MapFS{ + "config.json": &fstest.MapFile{ + Data: []byte(`{ + "replaced": { + "source": { + "kind": "http", + "path": "some-path" + }, + "sink": { + "kind": "debug" + } + } + }`), + }, + } + + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + time.Sleep(10 * time.Millisecond) + + res := testEvent("/-/some-path", payload, headers, router) + body := res.Body.String() + if body != "processed" { + t.Fatalf("unexpected response wanted processed, got: %s", body) + } + }) + + t.Run("config reloads without changes", func(t *testing.T) { + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + time.Sleep(10 * time.Millisecond) + + res := testEvent("/-/some-path", payload, headers, router) + body := res.Body.String() + if body != "processed" { + t.Fatalf("unexpected response wanted processed, got: %s", body) + } + }) +} + +func testEvent(path string, payload *bytes.Buffer, headers map[string]string, router *httprouter.Router) *httptest.ResponseRecorder { + res := httptest.NewRecorder() + logrus.Debugf("Calling %s", path) + req := httptest.NewRequest("POST", path, payload) + for k, v := range headers { + req.Header.Add(k, v) + } + router.ServeHTTP(res, req) + return res +} diff --git a/internal/source/types/types.go b/internal/source/types/types.go new file mode 100644 index 0000000..130d424 --- /dev/null +++ b/internal/source/types/types.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package types + +import ( + "encoding/json" + "errors" +) + +var ErrInvalid = errors.New("invalid source.kind provided") + +type Kind string + +const ( + HTTP Kind = "http" + Consul Kind = "consul" +) + +func (k *Kind) UnmarshalJSON(raw []byte) error { + var data string + if err := json.Unmarshal(raw, &data); err != nil { + return err + } + + nk := Kind(data) + switch nk { + case HTTP, Consul: + *k = nk + return nil + } + + return ErrInvalid +} diff --git a/internal/source/types/types_test.go b/internal/source/types/types_test.go new file mode 100644 index 0000000..615b2e4 --- /dev/null +++ b/internal/source/types/types_test.go @@ -0,0 +1,62 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package types_test + +import ( + "encoding/json" + "fmt" + "testing" + + "git.rob.mx/nidito/event-gateway/internal/source/types" +) + +func TestUmarshalJSON(t *testing.T) { + cases := []struct { + Name string + JSON string + Error error + Expected types.Kind + }{ + { + Name: "empty", + JSON: `""`, + Error: types.ErrInvalid, + }, + { + Name: "number", + JSON: `42`, + Error: fmt.Errorf("json: cannot unmarshal number into Go value of type string"), + }, + { + Name: "invalid", + JSON: `"42"`, + Error: types.ErrInvalid, + }, + { + Name: "valid", + JSON: `"consul"`, + Error: nil, + Expected: types.Consul, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + var res types.Kind + if err := json.Unmarshal([]byte(c.JSON), &res); err != nil { + if c.Error == nil { + t.Fatalf("unexpected error for %s: %s", c.Name, err) + } + + if err.Error() != c.Error.Error() { + t.Fatalf("unexpected error for %s:\n\twanted: %s\n\n\tgot : %s", c.Name, c.Error, err) + } + return + } + + if res != c.Expected { + t.Fatalf("did not parse into correct value, wanted: %s got: %s", c.Expected, res) + } + }) + } +} diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..eba1e0d --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,5 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package version + +var Version = "dev" diff --git a/main.go b/main.go new file mode 100644 index 0000000..3d9a8b8 --- /dev/null +++ b/main.go @@ -0,0 +1,139 @@ +// Copyright © 2023 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "fmt" + "log" + "net/http" + "os" + + "git.rob.mx/nidito/chinampa" + "git.rob.mx/nidito/chinampa/pkg/command" + "git.rob.mx/nidito/chinampa/pkg/env" + "git.rob.mx/nidito/chinampa/pkg/logger" + "git.rob.mx/nidito/chinampa/pkg/runtime" + "git.rob.mx/nidito/event-gateway/internal/config" + "git.rob.mx/nidito/event-gateway/internal/source" + "git.rob.mx/nidito/event-gateway/internal/source/consul" + httpsrc "git.rob.mx/nidito/event-gateway/internal/source/http" + "git.rob.mx/nidito/event-gateway/internal/version" + "github.com/hashicorp/consul/api" + "github.com/honeycombio/honeycomb-opentelemetry-go" + "github.com/honeycombio/otel-config-go/otelconfig" + "github.com/julienschmidt/httprouter" +) + +var Server = &command.Command{ + Path: []string{"server"}, + Summary: "starts a server", + Description: "tbd", + Arguments: command.Arguments{ + &command.Argument{ + Name: "listen", + Default: ":8000", + Description: "An address for the http server to listen at", + }, + }, + Options: command.Options{ + "source": &command.Option{ + Type: command.ValueTypeString, + Description: "Event sources to enable", + Repeated: true, + Default: []string{"http"}, + Values: &command.ValueSource{ + Static: &[]string{ + "http", + "consul", + }, + }, + }, + "sink": &command.Option{ + Type: command.ValueTypeString, + Description: "Event sinks to enable", + Repeated: true, + Default: []string{"debug", "nomad"}, + Values: &command.ValueSource{ + Static: &[]string{ + "debug", + "nomad", + }, + }, + }, + "config": &command.Option{ + Type: command.ValueTypeString, + Description: "A URL (file:// if no scheme is provided) where to read configuration from", + Default: "file://listeners.json", + }, + }, + Action: func(cmd *command.Command) error { + router := httprouter.New() + addr := cmd.Arguments[0].ToString() + manager := &source.Manager{} + + // Enable multi-span attributes + bsp := honeycomb.NewBaggageSpanProcessor() + // Use the Honeycomb distro to set up the OpenTelemetry SDK + otelShutdown, err := otelconfig.ConfigureOpenTelemetry( + otelconfig.WithSpanProcessor(bsp), + ) + if err != nil { + log.Fatalf("error setting up OTel SDK - %e", err) + } + defer otelShutdown() + + for _, source := range cmd.Options["source"].ToValue().([]string) { + switch source { + case "http": + manager.AddSource(&httpsrc.Source{Router: router}) + case "consul": + cfg := api.DefaultConfig() + client, err := api.NewClient(cfg) + + if err != nil { + return fmt.Errorf("could not create consul client: %s", err) + } + manager.AddSource(&consul.Source{Client: client.Event()}) + default: + logger.Fatalf("Unknown source type: %s", source) + } + logger.Infof("added source for %s events", source) + } + + cfg, err := config.FromURN(cmd.Options["config"].ToString()) + if err != nil { + return fmt.Errorf("could not initialize configuration loader: %s", err) + } + + if err := manager.Listen(cfg); err != nil { + return fmt.Errorf("could not start listener: %s", err) + } + + return http.ListenAndServe(addr, router) + }, +} + +func logLevel() logger.Level { + if os.Getenv(env.Debug) == "trace" { + return logger.LevelTrace + } else if runtime.DebugEnabled() { + return logger.LevelDebug + } + + return logger.LevelInfo +} + +func main() { + logger.Configure("event-gateway", logLevel()) + chinampa.Register(Server) + + if err := chinampa.Execute(chinampa.Config{ + Name: "event-gateway", + Summary: "routes events to places", + Description: `Does things TBD`, + Version: version.Version, + }); err != nil { + logger.Errorf("total failure: %s", err) + os.Exit(2) + } +}