POST /-/hello-world
This commit is contained in:
commit
e25fd89198
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -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
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
listeners.json
|
||||
.vscode/*
|
||||
coverage.*
|
||||
event-gateway
|
38
.golangci.yml
Normal file
38
.golangci.yml
Normal file
@ -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
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
@ -0,0 +1 @@
|
||||
golang 1.21.1
|
201
LICENSE.txt
Normal file
201
LICENSE.txt
Normal file
@ -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.
|
100
go.mod
Normal file
100
go.mod
Normal file
@ -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
|
||||
)
|
341
go.sum
Normal file
341
go.sum
Normal file
@ -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=
|
104
internal/config/config.go
Normal file
104
internal/config/config.go
Normal file
@ -0,0 +1,104 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
136
internal/config/config_test.go
Normal file
136
internal/config/config_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
99
internal/config/consul.go
Normal file
99
internal/config/consul.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
70
internal/config/file.go
Normal file
70
internal/config/file.go
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
||||
}()
|
||||
}
|
29
internal/payload/context.go
Normal file
29
internal/payload/context.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
34
internal/payload/payload.go
Normal file
34
internal/payload/payload.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
73
internal/sink/debug/debug.go
Normal file
73
internal/sink/debug/debug.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
118
internal/sink/nomad/nomad.go
Normal file
118
internal/sink/nomad/nomad.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
69
internal/sink/sink.go
Normal file
69
internal/sink/sink.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
32
internal/sink/types/types.go
Normal file
32
internal/sink/types/types.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
65
internal/source/consul/consul.go
Normal file
65
internal/source/consul/consul.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
||||
}
|
67
internal/source/consul/consul_test.go
Normal file
67
internal/source/consul/consul_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
||||
}
|
69
internal/source/consul/watch.go
Normal file
69
internal/source/consul/watch.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
128
internal/source/http/http.go
Normal file
128
internal/source/http/http.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
101
internal/source/http/http_test.go
Normal file
101
internal/source/http/http_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
||||
}
|
92
internal/source/http/payload.go
Normal file
92
internal/source/http/payload.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
166
internal/source/http/payload_test.go
Normal file
166
internal/source/http/payload_test.go
Normal file
@ -0,0 +1,166 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
111
internal/source/manager.go
Normal file
111
internal/source/manager.go
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
188
internal/source/manager_test.go
Normal file
188
internal/source/manager_test.go
Normal file
@ -0,0 +1,188 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
33
internal/source/types/types.go
Normal file
33
internal/source/types/types.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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
|
||||
}
|
62
internal/source/types/types_test.go
Normal file
62
internal/source/types/types_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
5
internal/version/version.go
Normal file
5
internal/version/version.go
Normal file
@ -0,0 +1,5 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package version
|
||||
|
||||
var Version = "dev"
|
139
main.go
Normal file
139
main.go
Normal file
@ -0,0 +1,139 @@
|
||||
// Copyright © 2023 Roberto Hidalgo <event-gateway@un.rob.mx>
|
||||
// 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user