discovery service
This commit is contained in:
72
.woodpecker/discovery.yml
Normal file
72
.woodpecker/discovery.yml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- DISCOVERY_IMAGE_PATH: discovery/service
|
||||||
|
DISCOVERY_DOCKERFILE: ci/prod/compose/discovery.dockerfile
|
||||||
|
DISCOVERY_ENV: prod
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: version
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- set -euo pipefail 2>/dev/null || set -eu
|
||||||
|
- apk add --no-cache git
|
||||||
|
- GIT_REV="$(git rev-parse --short HEAD)"
|
||||||
|
- BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
- APP_V="$(cat version)"
|
||||||
|
- BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
- BUILD_USER="${WOODPECKER_MACHINE:-woodpecker}"
|
||||||
|
- printf "GIT_REV=%s\nBUILD_BRANCH=%s\nAPP_V=%s\nBUILD_DATE=%s\nBUILD_USER=%s\n" \
|
||||||
|
"$GIT_REV" "$BUILD_BRANCH" "$APP_V" "$BUILD_DATE" "$BUILD_USER" | tee .env.version
|
||||||
|
|
||||||
|
- name: proto
|
||||||
|
image: golang:alpine
|
||||||
|
depends_on: [ version ]
|
||||||
|
commands:
|
||||||
|
- set -eu
|
||||||
|
- apk add --no-cache bash git build-base protoc protobuf-dev
|
||||||
|
- go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
|
- go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||||
|
- bash ci/scripts/proto/generate.sh
|
||||||
|
|
||||||
|
- name: secrets
|
||||||
|
image: alpine:latest
|
||||||
|
depends_on: [ version ]
|
||||||
|
environment:
|
||||||
|
VAULT_ADDR: { from_secret: VAULT_ADDR }
|
||||||
|
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
|
||||||
|
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
|
||||||
|
commands:
|
||||||
|
- set -euo pipefail
|
||||||
|
- apk add --no-cache bash coreutils openssh-keygen curl sed python3
|
||||||
|
- mkdir -p secrets
|
||||||
|
- ./ci/vlt kv_to_file kv ops/deploy/ssh_key private_b64 secrets/SSH_KEY.b64 600
|
||||||
|
- base64 -d secrets/SSH_KEY.b64 > secrets/SSH_KEY
|
||||||
|
- chmod 600 secrets/SSH_KEY
|
||||||
|
- ssh-keygen -y -f secrets/SSH_KEY >/dev/null
|
||||||
|
- ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER
|
||||||
|
- ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD
|
||||||
|
|
||||||
|
- name: build-image
|
||||||
|
image: gcr.io/kaniko-project/executor:debug
|
||||||
|
depends_on: [ proto, secrets ]
|
||||||
|
commands:
|
||||||
|
- sh ci/scripts/discovery/build-image.sh
|
||||||
|
|
||||||
|
- name: deploy
|
||||||
|
image: alpine:latest
|
||||||
|
depends_on: [ secrets, build-image ]
|
||||||
|
environment:
|
||||||
|
VAULT_ADDR: { from_secret: VAULT_ADDR }
|
||||||
|
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
|
||||||
|
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
|
||||||
|
commands:
|
||||||
|
- set -euo pipefail
|
||||||
|
- apk add --no-cache bash openssh-client rsync coreutils curl sed python3
|
||||||
|
- mkdir -p /root/.ssh
|
||||||
|
- install -m 600 secrets/SSH_KEY /root/.ssh/id_rsa
|
||||||
|
- sh ci/scripts/discovery/deploy.sh
|
||||||
3
api/discovery/.gitignore
vendored
Normal file
3
api/discovery/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
internal/generated
|
||||||
|
.gocache
|
||||||
|
app
|
||||||
17
api/discovery/config.yml
Normal file
17
api/discovery/config.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
runtime:
|
||||||
|
shutdown_timeout_seconds: 15
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
address: ":9405"
|
||||||
|
|
||||||
|
messaging:
|
||||||
|
driver: NATS
|
||||||
|
settings:
|
||||||
|
url_env: NATS_URL
|
||||||
|
host_env: NATS_HOST
|
||||||
|
port_env: NATS_PORT
|
||||||
|
username_env: NATS_USER
|
||||||
|
password_env: NATS_PASSWORD
|
||||||
|
broker_name: Discovery Service
|
||||||
|
max_reconnects: 10
|
||||||
|
reconnect_wait: 5
|
||||||
51
api/discovery/go.mod
Normal file
51
api/discovery/go.mod
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
module github.com/tech/sendico/discovery
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
replace github.com/tech/sendico/pkg => ../pkg
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
|
github.com/prometheus/client_golang v1.23.2
|
||||||
|
github.com/tech/sendico/pkg v0.1.0
|
||||||
|
go.uber.org/zap v1.27.1
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
||||||
|
github.com/casbin/casbin/v2 v2.135.0 // indirect
|
||||||
|
github.com/casbin/govaluate v1.10.0 // indirect
|
||||||
|
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
|
golang.org/x/net v0.48.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
|
golang.org/x/text v0.32.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||||
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
225
api/discovery/go.sum
Normal file
225
api/discovery/go.sum
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
|
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
|
||||||
|
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
|
||||||
|
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||||
|
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
|
||||||
|
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||||
|
github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E=
|
||||||
|
github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
|
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||||
|
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||||
|
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||||
|
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||||
|
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
|
||||||
|
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||||
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
|
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||||
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
|
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
|
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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||||
|
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||||
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
|
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/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
|
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
|
||||||
|
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
|
||||||
|
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||||
|
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||||
|
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
||||||
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
27
api/discovery/internal/appversion/version.go
Normal file
27
api/discovery/internal/appversion/version.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package appversion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/pkg/version"
|
||||||
|
vf "github.com/tech/sendico/pkg/version/factory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build information. Populated at build-time.
|
||||||
|
var (
|
||||||
|
Version string
|
||||||
|
Revision string
|
||||||
|
Branch string
|
||||||
|
BuildUser string
|
||||||
|
BuildDate string
|
||||||
|
)
|
||||||
|
|
||||||
|
func Create() version.Printer {
|
||||||
|
vi := version.Info{
|
||||||
|
Program: "Sendico Discovery Service",
|
||||||
|
Revision: Revision,
|
||||||
|
Branch: Branch,
|
||||||
|
BuildUser: BuildUser,
|
||||||
|
BuildDate: BuildDate,
|
||||||
|
Version: Version,
|
||||||
|
}
|
||||||
|
return vf.Create(&vi)
|
||||||
|
}
|
||||||
47
api/discovery/internal/server/internal/config.go
Normal file
47
api/discovery/internal/server/internal/config.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
|
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultMetricsAddress = ":9405"
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Runtime *grpcapp.RuntimeConfig `yaml:"runtime"`
|
||||||
|
Messaging *msg.Config `yaml:"messaging"`
|
||||||
|
Metrics *metricsConfig `yaml:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type metricsConfig struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) loadConfig() (*config, error) {
|
||||||
|
data, err := os.ReadFile(i.file)
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config{}
|
||||||
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||||
|
i.logger.Error("Failed to parse configuration", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Runtime == nil {
|
||||||
|
cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Metrics != nil && strings.TrimSpace(cfg.Metrics.Address) == "" {
|
||||||
|
cfg.Metrics.Address = defaultMetricsAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
58
api/discovery/internal/server/internal/discovery.go
Normal file
58
api/discovery/internal/server/internal/discovery.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/discovery/internal/appversion"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
|
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *Imp) startDiscovery(cfg *config) error {
|
||||||
|
if cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" {
|
||||||
|
return merrors.InvalidArgument("discovery service: messaging configuration is required", "messaging")
|
||||||
|
}
|
||||||
|
|
||||||
|
broker, err := msg.CreateMessagingBroker(i.logger.Named("discovery_bus"), cfg.Messaging)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.logger.Info("Discovery messaging broker ready", zap.String("messaging_driver", string(cfg.Messaging.Driver)))
|
||||||
|
producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker)
|
||||||
|
|
||||||
|
registry := discovery.NewRegistry()
|
||||||
|
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, string(mservice.Discovery))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
svc.Start()
|
||||||
|
i.registrySvc = svc
|
||||||
|
|
||||||
|
announce := discovery.Announcement{
|
||||||
|
Service: "DISCOVERY",
|
||||||
|
InstanceID: discovery.InstanceID(),
|
||||||
|
Operations: []string{"discovery.lookup"},
|
||||||
|
Version: appversion.Create().Short(),
|
||||||
|
}
|
||||||
|
i.announcer = discovery.NewAnnouncer(i.logger, producer, string(mservice.Discovery), announce)
|
||||||
|
i.announcer.Start()
|
||||||
|
|
||||||
|
i.logger.Info("Discovery registry service started", zap.String("messaging_driver", string(cfg.Messaging.Driver)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) stopDiscovery() {
|
||||||
|
if i == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if i.announcer != nil {
|
||||||
|
i.announcer.Stop()
|
||||||
|
i.announcer = nil
|
||||||
|
}
|
||||||
|
if i.registrySvc != nil {
|
||||||
|
i.registrySvc.Stop()
|
||||||
|
i.registrySvc = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
85
api/discovery/internal/server/internal/metrics.go
Normal file
85
api/discovery/internal/server/internal/metrics.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/health"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *Imp) startMetrics(cfg *metricsConfig) {
|
||||||
|
if i == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
address := ""
|
||||||
|
if cfg != nil {
|
||||||
|
address = strings.TrimSpace(cfg.Address)
|
||||||
|
}
|
||||||
|
if address == "" {
|
||||||
|
i.logger.Info("Metrics endpoint disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Error("Failed to bind metrics listener", zap.String("address", address), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
router := chi.NewRouter()
|
||||||
|
router.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
|
var healthRouter routers.Health
|
||||||
|
if hr, err := routers.NewHealthRouter(i.logger.Named("metrics"), router, ""); err != nil {
|
||||||
|
i.logger.Warn("Failed to initialise health router", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
hr.SetStatus(health.SSStarting)
|
||||||
|
healthRouter = hr
|
||||||
|
}
|
||||||
|
|
||||||
|
i.metricsHealth = healthRouter
|
||||||
|
i.metricsSrv = &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
Handler: router,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
if healthRouter != nil {
|
||||||
|
healthRouter.SetStatus(health.SSRunning)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
i.logger.Info("Prometheus endpoint listening", zap.String("address", address))
|
||||||
|
if err := i.metricsSrv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
i.logger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err))
|
||||||
|
if healthRouter != nil {
|
||||||
|
healthRouter.SetStatus(health.SSTerminating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) shutdownMetrics(ctx context.Context) {
|
||||||
|
if i.metricsHealth != nil {
|
||||||
|
i.metricsHealth.SetStatus(health.SSTerminating)
|
||||||
|
i.metricsHealth.Finish()
|
||||||
|
i.metricsHealth = nil
|
||||||
|
}
|
||||||
|
if i.metricsSrv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := i.metricsSrv.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
i.logger.Warn("Failed to stop metrics server", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
i.logger.Info("Metrics server stopped")
|
||||||
|
}
|
||||||
|
i.metricsSrv = nil
|
||||||
|
}
|
||||||
109
api/discovery/internal/server/internal/serverimp.go
Normal file
109
api/discovery/internal/server/internal/serverimp.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
|
||||||
|
return &Imp{
|
||||||
|
logger: logger.Named("server"),
|
||||||
|
file: file,
|
||||||
|
debug: debug,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) Start() error {
|
||||||
|
i.initStopChannels()
|
||||||
|
defer i.closeDone()
|
||||||
|
|
||||||
|
i.logger.Info("Starting discovery service", zap.String("config_file", i.file), zap.Bool("debug", i.debug))
|
||||||
|
|
||||||
|
cfg, err := i.loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.config = cfg
|
||||||
|
|
||||||
|
messagingDriver := "none"
|
||||||
|
if cfg.Messaging != nil {
|
||||||
|
messagingDriver = string(cfg.Messaging.Driver)
|
||||||
|
}
|
||||||
|
metricsAddress := ""
|
||||||
|
if cfg.Metrics != nil {
|
||||||
|
metricsAddress = strings.TrimSpace(cfg.Metrics.Address)
|
||||||
|
}
|
||||||
|
if metricsAddress == "" {
|
||||||
|
metricsAddress = "disabled"
|
||||||
|
}
|
||||||
|
i.logger.Info("Discovery config loaded", zap.String("messaging_driver", messagingDriver), zap.String("metrics_address", metricsAddress))
|
||||||
|
|
||||||
|
i.startMetrics(cfg.Metrics)
|
||||||
|
|
||||||
|
if err := i.startDiscovery(cfg); err != nil {
|
||||||
|
i.stopDiscovery()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), i.shutdownTimeout())
|
||||||
|
i.shutdownMetrics(ctx)
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.logger.Info("Discovery service ready", zap.String("messaging_driver", messagingDriver))
|
||||||
|
|
||||||
|
<-i.stopCh
|
||||||
|
i.logger.Info("Discovery service stop signal received")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) Shutdown() {
|
||||||
|
timeout := i.shutdownTimeout()
|
||||||
|
i.logger.Info("Stopping discovery service", zap.Duration("timeout", timeout))
|
||||||
|
|
||||||
|
i.stopDiscovery()
|
||||||
|
i.signalStop()
|
||||||
|
|
||||||
|
if i.doneCh != nil {
|
||||||
|
<-i.doneCh
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
i.shutdownMetrics(ctx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
i.logger.Info("Discovery service stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) initStopChannels() {
|
||||||
|
if i.stopCh == nil {
|
||||||
|
i.stopCh = make(chan struct{})
|
||||||
|
}
|
||||||
|
if i.doneCh == nil {
|
||||||
|
i.doneCh = make(chan struct{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) signalStop() {
|
||||||
|
i.stopOnce.Do(func() {
|
||||||
|
if i.stopCh != nil {
|
||||||
|
close(i.stopCh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) closeDone() {
|
||||||
|
i.doneOnce.Do(func() {
|
||||||
|
if i.doneCh != nil {
|
||||||
|
close(i.doneCh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) shutdownTimeout() time.Duration {
|
||||||
|
if i.config != nil && i.config.Runtime != nil {
|
||||||
|
return i.config.Runtime.ShutdownTimeout()
|
||||||
|
}
|
||||||
|
return 15 * time.Second
|
||||||
|
}
|
||||||
28
api/discovery/internal/server/internal/types.go
Normal file
28
api/discovery/internal/server/internal/types.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Imp struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
file string
|
||||||
|
debug bool
|
||||||
|
|
||||||
|
config *config
|
||||||
|
registrySvc *discovery.RegistryService
|
||||||
|
announcer *discovery.Announcer
|
||||||
|
|
||||||
|
metricsSrv *http.Server
|
||||||
|
metricsHealth routers.Health
|
||||||
|
|
||||||
|
stopOnce sync.Once
|
||||||
|
doneOnce sync.Once
|
||||||
|
stopCh chan struct{}
|
||||||
|
doneCh chan struct{}
|
||||||
|
}
|
||||||
11
api/discovery/internal/server/server.go
Normal file
11
api/discovery/internal/server/server.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
serverimp "github.com/tech/sendico/discovery/internal/server/internal"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
|
||||||
|
return serverimp.Create(logger, file, debug)
|
||||||
|
}
|
||||||
17
api/discovery/main.go
Normal file
17
api/discovery/main.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/discovery/internal/appversion"
|
||||||
|
si "github.com/tech/sendico/discovery/internal/server"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/server"
|
||||||
|
smain "github.com/tech/sendico/pkg/server/main"
|
||||||
|
)
|
||||||
|
|
||||||
|
func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
|
||||||
|
return si.Create(logger, file, debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
smain.RunServer("main", appversion.Create(), factory)
|
||||||
|
}
|
||||||
@@ -49,6 +49,18 @@ metrics:
|
|||||||
enabled: true
|
enabled: true
|
||||||
address: ":9102"
|
address: ":9102"
|
||||||
|
|
||||||
|
messaging:
|
||||||
|
driver: NATS
|
||||||
|
settings:
|
||||||
|
url_env: NATS_URL
|
||||||
|
host_env: NATS_HOST
|
||||||
|
port_env: NATS_PORT
|
||||||
|
username_env: NATS_USER
|
||||||
|
password_env: NATS_PASSWORD
|
||||||
|
broker_name: FX Ingestor
|
||||||
|
max_reconnects: 10
|
||||||
|
reconnect_wait: 5
|
||||||
|
|
||||||
database:
|
database:
|
||||||
driver: mongodb
|
driver: mongodb
|
||||||
settings:
|
settings:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/tech/sendico/fx/ingestor/internal/app"
|
"github.com/tech/sendico/fx/ingestor/internal/app"
|
||||||
"github.com/tech/sendico/fx/ingestor/internal/appversion"
|
"github.com/tech/sendico/fx/ingestor/internal/appversion"
|
||||||
"github.com/tech/sendico/fx/ingestor/internal/signalctx"
|
"github.com/tech/sendico/fx/ingestor/internal/signalctx"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
lf "github.com/tech/sendico/pkg/mlogger/factory"
|
lf "github.com/tech/sendico/pkg/mlogger/factory"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,7 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
logger := lf.NewLogger(*debugFlag).Named("fx_ingestor")
|
logger := lf.NewLogger(*debugFlag).Named("fx_ingestor")
|
||||||
|
logger = logger.With(zap.String("instance_id", discovery.InstanceID()))
|
||||||
defer logger.Sync()
|
defer logger.Sync()
|
||||||
|
|
||||||
av := appversion.Create()
|
av := appversion.Create()
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ api:
|
|||||||
message_broker:
|
message_broker:
|
||||||
driver: NATS
|
driver: NATS
|
||||||
settings:
|
settings:
|
||||||
|
url_env: NATS_URL
|
||||||
host_env: NATS_HOST
|
host_env: NATS_HOST
|
||||||
port_env: NATS_PORT
|
port_env: NATS_PORT
|
||||||
username_env: NATS_USER
|
username_env: NATS_USER
|
||||||
|
|||||||
224
api/payments/orchestrator/internal/server/internal/builders.go
Normal file
224
api/payments/orchestrator/internal/server/internal/builders.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
|
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||||
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||||
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildCardGatewayRoutes(src map[string]cardGatewayRouteConfig) map[string]orchestrator.CardGatewayRoute {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make(map[string]orchestrator.CardGatewayRoute, len(src))
|
||||||
|
for key, route := range src {
|
||||||
|
trimmedKey := strings.TrimSpace(key)
|
||||||
|
if trimmedKey == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[trimmedKey] = orchestrator.CardGatewayRoute{
|
||||||
|
FundingAddress: strings.TrimSpace(route.FundingAddress),
|
||||||
|
FeeAddress: strings.TrimSpace(route.FeeAddress),
|
||||||
|
FeeWalletRef: strings.TrimSpace(route.FeeWalletRef),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFeeLedgerAccounts(src map[string]string) map[string]string {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make(map[string]string, len(src))
|
||||||
|
for key, account := range src {
|
||||||
|
k := strings.ToLower(strings.TrimSpace(key))
|
||||||
|
v := strings.TrimSpace(account)
|
||||||
|
if k == "" || v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGatewayRegistry(logger mlogger.Logger, mntxClient mntxclient.Client, src []gatewayInstanceConfig, registry *discovery.Registry) orchestrator.GatewayRegistry {
|
||||||
|
static := buildGatewayInstances(logger, src)
|
||||||
|
staticRegistry := orchestrator.NewGatewayRegistry(logger, mntxClient, static)
|
||||||
|
discoveryRegistry := orchestrator.NewDiscoveryGatewayRegistry(logger, registry)
|
||||||
|
return orchestrator.NewCompositeGatewayRegistry(logger, staticRegistry, discoveryRegistry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRailGateways(chainClient chainclient.Client, src []gatewayInstanceConfig) map[string]rail.RailGateway {
|
||||||
|
if chainClient == nil || len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
instances := buildGatewayInstances(nil, src)
|
||||||
|
if len(instances) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := map[string]rail.RailGateway{}
|
||||||
|
for _, inst := range instances {
|
||||||
|
if inst == nil || !inst.IsEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inst.Rail != model.RailCrypto {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfg := chainclient.RailGatewayConfig{
|
||||||
|
Rail: string(inst.Rail),
|
||||||
|
Network: inst.Network,
|
||||||
|
Capabilities: rail.RailCapabilities{
|
||||||
|
CanPayIn: inst.Capabilities.CanPayIn,
|
||||||
|
CanPayOut: inst.Capabilities.CanPayOut,
|
||||||
|
CanReadBalance: inst.Capabilities.CanReadBalance,
|
||||||
|
CanSendFee: inst.Capabilities.CanSendFee,
|
||||||
|
RequiresObserveConfirm: inst.Capabilities.RequiresObserveConfirm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result[inst.ID] = chainclient.NewRailGateway(chainClient, cfg)
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGatewayInstances(logger mlogger.Logger, src []gatewayInstanceConfig) []*model.GatewayInstanceDescriptor {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if logger != nil {
|
||||||
|
logger = logger.Named("gateway_instances")
|
||||||
|
}
|
||||||
|
result := make([]*model.GatewayInstanceDescriptor, 0, len(src))
|
||||||
|
for _, cfg := range src {
|
||||||
|
id := strings.TrimSpace(cfg.ID)
|
||||||
|
if id == "" {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Warn("Gateway instance skipped: missing id")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rail := parseRail(cfg.Rail)
|
||||||
|
if rail == model.RailUnspecified {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Warn("Gateway instance skipped: invalid rail", zap.String("id", id), zap.String("rail", cfg.Rail))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
enabled := true
|
||||||
|
if cfg.IsEnabled != nil {
|
||||||
|
enabled = *cfg.IsEnabled
|
||||||
|
}
|
||||||
|
result = append(result, &model.GatewayInstanceDescriptor{
|
||||||
|
ID: id,
|
||||||
|
Rail: rail,
|
||||||
|
Network: strings.ToUpper(strings.TrimSpace(cfg.Network)),
|
||||||
|
Currencies: normalizeCurrencies(cfg.Currencies),
|
||||||
|
Capabilities: model.RailCapabilities{
|
||||||
|
CanPayIn: cfg.Capabilities.CanPayIn,
|
||||||
|
CanPayOut: cfg.Capabilities.CanPayOut,
|
||||||
|
CanReadBalance: cfg.Capabilities.CanReadBalance,
|
||||||
|
CanSendFee: cfg.Capabilities.CanSendFee,
|
||||||
|
RequiresObserveConfirm: cfg.Capabilities.RequiresObserveConfirm,
|
||||||
|
},
|
||||||
|
Limits: buildGatewayLimits(cfg.Limits),
|
||||||
|
Version: strings.TrimSpace(cfg.Version),
|
||||||
|
IsEnabled: enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRail(value string) model.Rail {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case string(model.RailCrypto):
|
||||||
|
return model.RailCrypto
|
||||||
|
case string(model.RailProviderSettlement):
|
||||||
|
return model.RailProviderSettlement
|
||||||
|
case string(model.RailLedger):
|
||||||
|
return model.RailLedger
|
||||||
|
case string(model.RailCardPayout):
|
||||||
|
return model.RailCardPayout
|
||||||
|
case string(model.RailFiatOnRamp):
|
||||||
|
return model.RailFiatOnRamp
|
||||||
|
default:
|
||||||
|
return model.RailUnspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCurrencies(values []string) []string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
result := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
clean := strings.ToUpper(strings.TrimSpace(value))
|
||||||
|
if clean == "" || seen[clean] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[clean] = true
|
||||||
|
result = append(result, clean)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGatewayLimits(cfg limitsConfig) model.Limits {
|
||||||
|
limits := model.Limits{
|
||||||
|
MinAmount: strings.TrimSpace(cfg.MinAmount),
|
||||||
|
MaxAmount: strings.TrimSpace(cfg.MaxAmount),
|
||||||
|
PerTxMaxFee: strings.TrimSpace(cfg.PerTxMaxFee),
|
||||||
|
PerTxMinAmount: strings.TrimSpace(cfg.PerTxMinAmount),
|
||||||
|
PerTxMaxAmount: strings.TrimSpace(cfg.PerTxMaxAmount),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.VolumeLimit) > 0 {
|
||||||
|
limits.VolumeLimit = map[string]string{}
|
||||||
|
for key, value := range cfg.VolumeLimit {
|
||||||
|
bucket := strings.TrimSpace(key)
|
||||||
|
amount := strings.TrimSpace(value)
|
||||||
|
if bucket == "" || amount == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
limits.VolumeLimit[bucket] = amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.VelocityLimit) > 0 {
|
||||||
|
limits.VelocityLimit = map[string]int{}
|
||||||
|
for key, value := range cfg.VelocityLimit {
|
||||||
|
bucket := strings.TrimSpace(key)
|
||||||
|
if bucket == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
limits.VelocityLimit[bucket] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.CurrencyLimits) > 0 {
|
||||||
|
limits.CurrencyLimits = map[string]model.LimitsOverride{}
|
||||||
|
for key, override := range cfg.CurrencyLimits {
|
||||||
|
currency := strings.ToUpper(strings.TrimSpace(key))
|
||||||
|
if currency == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
limits.CurrencyLimits[currency] = model.LimitsOverride{
|
||||||
|
MaxVolume: strings.TrimSpace(override.MaxVolume),
|
||||||
|
MinAmount: strings.TrimSpace(override.MinAmount),
|
||||||
|
MaxAmount: strings.TrimSpace(override.MaxAmount),
|
||||||
|
MaxFee: strings.TrimSpace(override.MaxFee),
|
||||||
|
MaxOps: override.MaxOps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return limits
|
||||||
|
}
|
||||||
150
api/payments/orchestrator/internal/server/internal/clients.go
Normal file
150
api/payments/orchestrator/internal/server/internal/clients.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||||
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
|
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||||
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *Imp) initFeesClient(cfg clientConfig) (feesv1.FeeEngineClient, *grpc.ClientConn) {
|
||||||
|
addr := cfg.address()
|
||||||
|
if addr == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dialCtx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
creds := credentials.NewTLS(&tls.Config{})
|
||||||
|
if cfg.InsecureTransport {
|
||||||
|
creds = insecure.NewCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(dialCtx, addr, grpc.WithTransportCredentials(creds))
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Warn("Failed to connect to fees service", zap.String("address", addr), zap.Error(err))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
i.logger.Info("Connected to fees service", zap.String("address", addr))
|
||||||
|
return feesv1.NewFeeEngineClient(conn), conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) initLedgerClient(cfg clientConfig) ledgerclient.Client {
|
||||||
|
addr := cfg.address()
|
||||||
|
if addr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := ledgerclient.New(ctx, ledgerclient.Config{
|
||||||
|
Address: addr,
|
||||||
|
DialTimeout: cfg.dialTimeout(),
|
||||||
|
CallTimeout: cfg.callTimeout(),
|
||||||
|
Insecure: cfg.InsecureTransport,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Warn("Failed to connect to ledger service", zap.String("address", addr), zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i.logger.Info("Connected to ledger service", zap.String("address", addr))
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) initGatewayClient(cfg clientConfig) chainclient.Client {
|
||||||
|
addr := cfg.address()
|
||||||
|
if addr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := chainclient.New(ctx, chainclient.Config{
|
||||||
|
Address: addr,
|
||||||
|
DialTimeout: cfg.dialTimeout(),
|
||||||
|
CallTimeout: cfg.callTimeout(),
|
||||||
|
Insecure: cfg.InsecureTransport,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Warn("failed to connect to chain gateway service", zap.String("address", addr), zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i.logger.Info("connected to chain gateway service", zap.String("address", addr))
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) initMntxClient(cfg clientConfig) mntxclient.Client {
|
||||||
|
addr := cfg.address()
|
||||||
|
if addr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := mntxclient.New(ctx, mntxclient.Config{
|
||||||
|
Address: addr,
|
||||||
|
DialTimeout: cfg.dialTimeout(),
|
||||||
|
CallTimeout: cfg.callTimeout(),
|
||||||
|
Logger: i.logger.Named("client.mntx"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Warn("Failed to connect to mntx gateway service", zap.String("address", addr), zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i.logger.Info("Connected to mntx gateway service", zap.String("address", addr))
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) initOracleClient(cfg clientConfig) oracleclient.Client {
|
||||||
|
addr := cfg.address()
|
||||||
|
if addr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := oracleclient.New(ctx, oracleclient.Config{
|
||||||
|
Address: addr,
|
||||||
|
DialTimeout: cfg.dialTimeout(),
|
||||||
|
CallTimeout: cfg.callTimeout(),
|
||||||
|
Insecure: cfg.InsecureTransport,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Warn("Failed to connect to oracle service", zap.String("address", addr), zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i.logger.Info("Connected to oracle service", zap.String("address", addr))
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) closeClients() {
|
||||||
|
if i.ledgerClient != nil {
|
||||||
|
_ = i.ledgerClient.Close()
|
||||||
|
}
|
||||||
|
if i.gatewayClient != nil {
|
||||||
|
_ = i.gatewayClient.Close()
|
||||||
|
}
|
||||||
|
if i.mntxClient != nil {
|
||||||
|
_ = i.mntxClient.Close()
|
||||||
|
}
|
||||||
|
if i.oracleClient != nil {
|
||||||
|
_ = i.oracleClient.Close()
|
||||||
|
}
|
||||||
|
if i.feesConn != nil {
|
||||||
|
_ = i.feesConn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
135
api/payments/orchestrator/internal/server/internal/config.go
Normal file
135
api/payments/orchestrator/internal/server/internal/config.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
|
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
*grpcapp.Config `yaml:",inline"`
|
||||||
|
Fees clientConfig `yaml:"fees"`
|
||||||
|
Ledger clientConfig `yaml:"ledger"`
|
||||||
|
Gateway clientConfig `yaml:"gateway"`
|
||||||
|
Mntx clientConfig `yaml:"mntx"`
|
||||||
|
Oracle clientConfig `yaml:"oracle"`
|
||||||
|
CardGateways map[string]cardGatewayRouteConfig `yaml:"card_gateways"`
|
||||||
|
FeeAccounts map[string]string `yaml:"fee_ledger_accounts"`
|
||||||
|
GatewayInstances []gatewayInstanceConfig `yaml:"gateway_instances"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientConfig struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
DialTimeoutSecs int `yaml:"dial_timeout_seconds"`
|
||||||
|
CallTimeoutSecs int `yaml:"call_timeout_seconds"`
|
||||||
|
InsecureTransport bool `yaml:"insecure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cardGatewayRouteConfig struct {
|
||||||
|
FundingAddress string `yaml:"funding_address"`
|
||||||
|
FeeAddress string `yaml:"fee_address"`
|
||||||
|
FeeWalletRef string `yaml:"fee_wallet_ref"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type gatewayInstanceConfig struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Rail string `yaml:"rail"`
|
||||||
|
Network string `yaml:"network"`
|
||||||
|
Currencies []string `yaml:"currencies"`
|
||||||
|
Capabilities gatewayCapabilitiesConfig `yaml:"capabilities"`
|
||||||
|
Limits limitsConfig `yaml:"limits"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
IsEnabled *bool `yaml:"is_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type gatewayCapabilitiesConfig struct {
|
||||||
|
CanPayIn bool `yaml:"can_pay_in"`
|
||||||
|
CanPayOut bool `yaml:"can_pay_out"`
|
||||||
|
CanReadBalance bool `yaml:"can_read_balance"`
|
||||||
|
CanSendFee bool `yaml:"can_send_fee"`
|
||||||
|
RequiresObserveConfirm bool `yaml:"requires_observe_confirm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type limitsConfig struct {
|
||||||
|
MinAmount string `yaml:"min_amount"`
|
||||||
|
MaxAmount string `yaml:"max_amount"`
|
||||||
|
PerTxMaxFee string `yaml:"per_tx_max_fee"`
|
||||||
|
PerTxMinAmount string `yaml:"per_tx_min_amount"`
|
||||||
|
PerTxMaxAmount string `yaml:"per_tx_max_amount"`
|
||||||
|
VolumeLimit map[string]string `yaml:"volume_limit"`
|
||||||
|
VelocityLimit map[string]int `yaml:"velocity_limit"`
|
||||||
|
CurrencyLimits map[string]limitsOverrideCfg `yaml:"currency_limits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type limitsOverrideCfg struct {
|
||||||
|
MaxVolume string `yaml:"max_volume"`
|
||||||
|
MinAmount string `yaml:"min_amount"`
|
||||||
|
MaxAmount string `yaml:"max_amount"`
|
||||||
|
MaxFee string `yaml:"max_fee"`
|
||||||
|
MaxOps int `yaml:"max_ops"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c clientConfig) address() string {
|
||||||
|
return strings.TrimSpace(c.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c clientConfig) dialTimeout() time.Duration {
|
||||||
|
if c.DialTimeoutSecs <= 0 {
|
||||||
|
return 5 * time.Second
|
||||||
|
}
|
||||||
|
return time.Duration(c.DialTimeoutSecs) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c clientConfig) callTimeout() time.Duration {
|
||||||
|
if c.CallTimeoutSecs <= 0 {
|
||||||
|
return 3 * time.Second
|
||||||
|
}
|
||||||
|
return time.Duration(c.CallTimeoutSecs) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) loadConfig() (*config, error) {
|
||||||
|
data, err := os.ReadFile(i.file)
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config{Config: &grpcapp.Config{}}
|
||||||
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||||
|
i.logger.Error("Failed to parse configuration", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Runtime == nil {
|
||||||
|
cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.GRPC == nil {
|
||||||
|
cfg.GRPC = &routers.GRPCConfig{
|
||||||
|
Network: "tcp",
|
||||||
|
Address: ":50062",
|
||||||
|
EnableReflection: true,
|
||||||
|
EnableHealth: true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if strings.TrimSpace(cfg.GRPC.Address) == "" {
|
||||||
|
cfg.GRPC.Address = ":50062"
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.GRPC.Network) == "" {
|
||||||
|
cfg.GRPC.Network = "tcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Metrics == nil {
|
||||||
|
cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9403"}
|
||||||
|
} else if strings.TrimSpace(cfg.Metrics.Address) == "" {
|
||||||
|
cfg.Metrics.Address = ":9403"
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||||
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
|
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||||
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||||
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type orchestratorDeps struct {
|
||||||
|
feesClient feesv1.FeeEngineClient
|
||||||
|
ledgerClient ledgerclient.Client
|
||||||
|
gatewayClient chainclient.Client
|
||||||
|
mntxClient mntxclient.Client
|
||||||
|
oracleClient oracleclient.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) initDependencies(cfg *config) *orchestratorDeps {
|
||||||
|
deps := &orchestratorDeps{}
|
||||||
|
if cfg == nil {
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.feesClient, i.feesConn = i.initFeesClient(cfg.Fees)
|
||||||
|
|
||||||
|
deps.ledgerClient = i.initLedgerClient(cfg.Ledger)
|
||||||
|
if deps.ledgerClient != nil {
|
||||||
|
i.ledgerClient = deps.ledgerClient
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.gatewayClient = i.initGatewayClient(cfg.Gateway)
|
||||||
|
if deps.gatewayClient != nil {
|
||||||
|
i.gatewayClient = deps.gatewayClient
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.mntxClient = i.initMntxClient(cfg.Mntx)
|
||||||
|
if deps.mntxClient != nil {
|
||||||
|
i.mntxClient = deps.mntxClient
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.oracleClient = i.initOracleClient(cfg.Oracle)
|
||||||
|
if deps.oracleClient != nil {
|
||||||
|
i.oracleClient = deps.oracleClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchestrator.Option {
|
||||||
|
if cfg == nil || deps == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
opts := []orchestrator.Option{}
|
||||||
|
if deps.feesClient != nil {
|
||||||
|
opts = append(opts, orchestrator.WithFeeEngine(deps.feesClient, cfg.Fees.callTimeout()))
|
||||||
|
}
|
||||||
|
if deps.ledgerClient != nil {
|
||||||
|
opts = append(opts, orchestrator.WithLedgerClient(deps.ledgerClient))
|
||||||
|
}
|
||||||
|
if deps.gatewayClient != nil {
|
||||||
|
opts = append(opts, orchestrator.WithChainGatewayClient(deps.gatewayClient))
|
||||||
|
}
|
||||||
|
if railGateways := buildRailGateways(deps.gatewayClient, cfg.GatewayInstances); len(railGateways) > 0 {
|
||||||
|
opts = append(opts, orchestrator.WithRailGateways(railGateways))
|
||||||
|
}
|
||||||
|
if deps.mntxClient != nil {
|
||||||
|
opts = append(opts, orchestrator.WithMntxGateway(deps.mntxClient))
|
||||||
|
}
|
||||||
|
if deps.oracleClient != nil {
|
||||||
|
opts = append(opts, orchestrator.WithOracleClient(deps.oracleClient))
|
||||||
|
}
|
||||||
|
if routes := buildCardGatewayRoutes(cfg.CardGateways); len(routes) > 0 {
|
||||||
|
opts = append(opts, orchestrator.WithCardGatewayRoutes(routes))
|
||||||
|
}
|
||||||
|
if feeAccounts := buildFeeLedgerAccounts(cfg.FeeAccounts); len(feeAccounts) > 0 {
|
||||||
|
opts = append(opts, orchestrator.WithFeeLedgerAccounts(feeAccounts))
|
||||||
|
}
|
||||||
|
if registry := buildGatewayRegistry(i.logger, deps.mntxClient, cfg.GatewayInstances, i.discoveryReg); registry != nil {
|
||||||
|
opts = append(opts, orchestrator.WithGatewayRegistry(registry))
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/payments/orchestrator/internal/appversion"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
|
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *Imp) initDiscovery(cfg *config) {
|
||||||
|
if cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger := i.logger.Named("discovery")
|
||||||
|
broker, err := msg.CreateMessagingBroker(logger.Named("bus"), cfg.Messaging)
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Warn("Failed to initialise discovery broker", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
producer := msgproducer.NewProducer(logger.Named("producer"), broker)
|
||||||
|
registry := discovery.NewRegistry()
|
||||||
|
watcher, err := discovery.NewRegistryWatcher(i.logger, broker, registry)
|
||||||
|
if err != nil {
|
||||||
|
i.logger.Warn("Failed to initialise discovery registry watcher", zap.Error(err))
|
||||||
|
} else if err := watcher.Start(); err != nil {
|
||||||
|
i.logger.Warn("Failed to start discovery registry watcher", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
i.discoveryWatcher = watcher
|
||||||
|
i.discoveryReg = registry
|
||||||
|
i.logger.Info("Discovery registry watcher started")
|
||||||
|
}
|
||||||
|
announce := discovery.Announcement{
|
||||||
|
Service: "PAYMENTS_ORCHESTRATOR",
|
||||||
|
Operations: []string{"payment.quote", "payment.initiate"},
|
||||||
|
Version: appversion.Create().Short(),
|
||||||
|
}
|
||||||
|
i.discoveryAnnouncer = discovery.NewAnnouncer(i.logger, producer, string(mservice.PaymentOrchestrator), announce)
|
||||||
|
i.discoveryAnnouncer.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) stopDiscovery() {
|
||||||
|
if i.discoveryAnnouncer != nil {
|
||||||
|
i.discoveryAnnouncer.Stop()
|
||||||
|
}
|
||||||
|
if i.discoveryWatcher != nil {
|
||||||
|
i.discoveryWatcher.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *Imp) shutdownApp() {
|
||||||
|
if i.app != nil {
|
||||||
|
timeout := 15 * time.Second
|
||||||
|
if i.config != nil && i.config.Runtime != nil {
|
||||||
|
timeout = i.config.Runtime.ShutdownTimeout()
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
i.app.Shutdown(ctx)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,136 +1,15 @@
|
|||||||
package serverimp
|
package serverimp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
|
||||||
chainclient "github.com/tech/sendico/gateway/chain/client"
|
|
||||||
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
|
||||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/appversion"
|
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage"
|
"github.com/tech/sendico/payments/orchestrator/storage"
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
|
||||||
mongostorage "github.com/tech/sendico/payments/orchestrator/storage/mongo"
|
mongostorage "github.com/tech/sendico/payments/orchestrator/storage/mongo"
|
||||||
"github.com/tech/sendico/pkg/api/routers"
|
|
||||||
"github.com/tech/sendico/pkg/db"
|
"github.com/tech/sendico/pkg/db"
|
||||||
"github.com/tech/sendico/pkg/discovery"
|
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
|
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
|
||||||
"github.com/tech/sendico/pkg/server/grpcapp"
|
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||||
"go.uber.org/zap"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Imp struct {
|
|
||||||
logger mlogger.Logger
|
|
||||||
file string
|
|
||||||
debug bool
|
|
||||||
|
|
||||||
config *config
|
|
||||||
app *grpcapp.App[storage.Repository]
|
|
||||||
discoverySvc *discovery.RegistryService
|
|
||||||
discoveryReg *discovery.Registry
|
|
||||||
discoveryAnnouncer *discovery.Announcer
|
|
||||||
feesConn *grpc.ClientConn
|
|
||||||
ledgerClient ledgerclient.Client
|
|
||||||
gatewayClient chainclient.Client
|
|
||||||
mntxClient mntxclient.Client
|
|
||||||
oracleClient oracleclient.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
*grpcapp.Config `yaml:",inline"`
|
|
||||||
Fees clientConfig `yaml:"fees"`
|
|
||||||
Ledger clientConfig `yaml:"ledger"`
|
|
||||||
Gateway clientConfig `yaml:"gateway"`
|
|
||||||
Mntx clientConfig `yaml:"mntx"`
|
|
||||||
Oracle clientConfig `yaml:"oracle"`
|
|
||||||
CardGateways map[string]cardGatewayRouteConfig `yaml:"card_gateways"`
|
|
||||||
FeeAccounts map[string]string `yaml:"fee_ledger_accounts"`
|
|
||||||
GatewayInstances []gatewayInstanceConfig `yaml:"gateway_instances"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientConfig struct {
|
|
||||||
Address string `yaml:"address"`
|
|
||||||
DialTimeoutSecs int `yaml:"dial_timeout_seconds"`
|
|
||||||
CallTimeoutSecs int `yaml:"call_timeout_seconds"`
|
|
||||||
InsecureTransport bool `yaml:"insecure"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type cardGatewayRouteConfig struct {
|
|
||||||
FundingAddress string `yaml:"funding_address"`
|
|
||||||
FeeAddress string `yaml:"fee_address"`
|
|
||||||
FeeWalletRef string `yaml:"fee_wallet_ref"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type gatewayInstanceConfig struct {
|
|
||||||
ID string `yaml:"id"`
|
|
||||||
Rail string `yaml:"rail"`
|
|
||||||
Network string `yaml:"network"`
|
|
||||||
Currencies []string `yaml:"currencies"`
|
|
||||||
Capabilities gatewayCapabilitiesConfig `yaml:"capabilities"`
|
|
||||||
Limits limitsConfig `yaml:"limits"`
|
|
||||||
Version string `yaml:"version"`
|
|
||||||
IsEnabled *bool `yaml:"is_enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type gatewayCapabilitiesConfig struct {
|
|
||||||
CanPayIn bool `yaml:"can_pay_in"`
|
|
||||||
CanPayOut bool `yaml:"can_pay_out"`
|
|
||||||
CanReadBalance bool `yaml:"can_read_balance"`
|
|
||||||
CanSendFee bool `yaml:"can_send_fee"`
|
|
||||||
RequiresObserveConfirm bool `yaml:"requires_observe_confirm"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type limitsConfig struct {
|
|
||||||
MinAmount string `yaml:"min_amount"`
|
|
||||||
MaxAmount string `yaml:"max_amount"`
|
|
||||||
PerTxMaxFee string `yaml:"per_tx_max_fee"`
|
|
||||||
PerTxMinAmount string `yaml:"per_tx_min_amount"`
|
|
||||||
PerTxMaxAmount string `yaml:"per_tx_max_amount"`
|
|
||||||
VolumeLimit map[string]string `yaml:"volume_limit"`
|
|
||||||
VelocityLimit map[string]int `yaml:"velocity_limit"`
|
|
||||||
CurrencyLimits map[string]limitsOverrideCfg `yaml:"currency_limits"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type limitsOverrideCfg struct {
|
|
||||||
MaxVolume string `yaml:"max_volume"`
|
|
||||||
MinAmount string `yaml:"min_amount"`
|
|
||||||
MaxAmount string `yaml:"max_amount"`
|
|
||||||
MaxFee string `yaml:"max_fee"`
|
|
||||||
MaxOps int `yaml:"max_ops"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c clientConfig) address() string {
|
|
||||||
return strings.TrimSpace(c.Address)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c clientConfig) dialTimeout() time.Duration {
|
|
||||||
if c.DialTimeoutSecs <= 0 {
|
|
||||||
return 5 * time.Second
|
|
||||||
}
|
|
||||||
return time.Duration(c.DialTimeoutSecs) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c clientConfig) callTimeout() time.Duration {
|
|
||||||
if c.CallTimeoutSecs <= 0 {
|
|
||||||
return 3 * time.Second
|
|
||||||
}
|
|
||||||
return time.Duration(c.CallTimeoutSecs) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
|
func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
|
||||||
return &Imp{
|
return &Imp{
|
||||||
logger: logger.Named("server"),
|
logger: logger.Named("server"),
|
||||||
@@ -140,37 +19,9 @@ func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Imp) Shutdown() {
|
func (i *Imp) Shutdown() {
|
||||||
if i.discoveryAnnouncer != nil {
|
i.stopDiscovery()
|
||||||
i.discoveryAnnouncer.Stop()
|
i.shutdownApp()
|
||||||
}
|
i.closeClients()
|
||||||
if i.discoverySvc != nil {
|
|
||||||
i.discoverySvc.Stop()
|
|
||||||
}
|
|
||||||
if i.app != nil {
|
|
||||||
timeout := 15 * time.Second
|
|
||||||
if i.config != nil && i.config.Runtime != nil {
|
|
||||||
timeout = i.config.Runtime.ShutdownTimeout()
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
i.app.Shutdown(ctx)
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
if i.ledgerClient != nil {
|
|
||||||
_ = i.ledgerClient.Close()
|
|
||||||
}
|
|
||||||
if i.gatewayClient != nil {
|
|
||||||
_ = i.gatewayClient.Close()
|
|
||||||
}
|
|
||||||
if i.mntxClient != nil {
|
|
||||||
_ = i.mntxClient.Close()
|
|
||||||
}
|
|
||||||
if i.oracleClient != nil {
|
|
||||||
_ = i.oracleClient.Close()
|
|
||||||
}
|
|
||||||
if i.feesConn != nil {
|
|
||||||
_ = i.feesConn.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Imp) Start() error {
|
func (i *Imp) Start() error {
|
||||||
@@ -180,90 +31,16 @@ func (i *Imp) Start() error {
|
|||||||
}
|
}
|
||||||
i.config = cfg
|
i.config = cfg
|
||||||
|
|
||||||
if cfg.Messaging != nil && cfg.Messaging.Driver != "" {
|
i.initDiscovery(cfg)
|
||||||
broker, err := msg.CreateMessagingBroker(i.logger.Named("discovery_bus"), cfg.Messaging)
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to initialise discovery broker", zap.Error(err))
|
|
||||||
} else {
|
|
||||||
producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker)
|
|
||||||
registry := discovery.NewRegistry()
|
|
||||||
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, string(mservice.PaymentOrchestrator))
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to start discovery registry service", zap.Error(err))
|
|
||||||
} else {
|
|
||||||
svc.Start()
|
|
||||||
i.discoverySvc = svc
|
|
||||||
i.discoveryReg = registry
|
|
||||||
i.logger.Info("Discovery registry service started")
|
|
||||||
}
|
|
||||||
announce := discovery.Announcement{
|
|
||||||
Service: "PAYMENTS_ORCHESTRATOR",
|
|
||||||
Operations: []string{"payment.quote", "payment.initiate"},
|
|
||||||
Version: appversion.Create().Short(),
|
|
||||||
}
|
|
||||||
i.discoveryAnnouncer = discovery.NewAnnouncer(i.logger, producer, string(mservice.PaymentOrchestrator), announce)
|
|
||||||
i.discoveryAnnouncer.Start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) {
|
repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) {
|
||||||
return mongostorage.New(logger, conn)
|
return mongostorage.New(logger, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
feesClient, feesConn := i.initFeesClient(cfg.Fees)
|
deps := i.initDependencies(cfg)
|
||||||
if feesConn != nil {
|
|
||||||
i.feesConn = feesConn
|
|
||||||
}
|
|
||||||
|
|
||||||
ledgerClient := i.initLedgerClient(cfg.Ledger)
|
|
||||||
if ledgerClient != nil {
|
|
||||||
i.ledgerClient = ledgerClient
|
|
||||||
}
|
|
||||||
|
|
||||||
gatewayClient := i.initGatewayClient(cfg.Gateway)
|
|
||||||
if gatewayClient != nil {
|
|
||||||
i.gatewayClient = gatewayClient
|
|
||||||
}
|
|
||||||
|
|
||||||
mntxClient := i.initMntxClient(cfg.Mntx)
|
|
||||||
if mntxClient != nil {
|
|
||||||
i.mntxClient = mntxClient
|
|
||||||
}
|
|
||||||
|
|
||||||
oracleClient := i.initOracleClient(cfg.Oracle)
|
|
||||||
if oracleClient != nil {
|
|
||||||
i.oracleClient = oracleClient
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
|
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
|
||||||
opts := []orchestrator.Option{}
|
opts := i.buildServiceOptions(cfg, deps)
|
||||||
if feesClient != nil {
|
|
||||||
opts = append(opts, orchestrator.WithFeeEngine(feesClient, cfg.Fees.callTimeout()))
|
|
||||||
}
|
|
||||||
if ledgerClient != nil {
|
|
||||||
opts = append(opts, orchestrator.WithLedgerClient(ledgerClient))
|
|
||||||
}
|
|
||||||
if gatewayClient != nil {
|
|
||||||
opts = append(opts, orchestrator.WithChainGatewayClient(gatewayClient))
|
|
||||||
}
|
|
||||||
if railGateways := buildRailGateways(gatewayClient, cfg.GatewayInstances); len(railGateways) > 0 {
|
|
||||||
opts = append(opts, orchestrator.WithRailGateways(railGateways))
|
|
||||||
}
|
|
||||||
if mntxClient != nil {
|
|
||||||
opts = append(opts, orchestrator.WithMntxGateway(mntxClient))
|
|
||||||
}
|
|
||||||
if oracleClient != nil {
|
|
||||||
opts = append(opts, orchestrator.WithOracleClient(oracleClient))
|
|
||||||
}
|
|
||||||
if routes := buildCardGatewayRoutes(cfg.CardGateways); len(routes) > 0 {
|
|
||||||
opts = append(opts, orchestrator.WithCardGatewayRoutes(routes))
|
|
||||||
}
|
|
||||||
if feeAccounts := buildFeeLedgerAccounts(cfg.FeeAccounts); len(feeAccounts) > 0 {
|
|
||||||
opts = append(opts, orchestrator.WithFeeLedgerAccounts(feeAccounts))
|
|
||||||
}
|
|
||||||
if registry := buildGatewayRegistry(i.logger, mntxClient, cfg.GatewayInstances, i.discoveryReg); registry != nil {
|
|
||||||
opts = append(opts, orchestrator.WithGatewayRegistry(registry))
|
|
||||||
}
|
|
||||||
return orchestrator.NewService(logger, repo, opts...), nil
|
return orchestrator.NewService(logger, repo, opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,371 +52,3 @@ func (i *Imp) Start() error {
|
|||||||
|
|
||||||
return i.app.Start()
|
return i.app.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Imp) initFeesClient(cfg clientConfig) (feesv1.FeeEngineClient, *grpc.ClientConn) {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dialCtx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
creds := credentials.NewTLS(&tls.Config{})
|
|
||||||
if cfg.InsecureTransport {
|
|
||||||
creds = insecure.NewCredentials()
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := grpc.DialContext(dialCtx, addr, grpc.WithTransportCredentials(creds))
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to connect to fees service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
i.logger.Info("Connected to fees service", zap.String("address", addr))
|
|
||||||
return feesv1.NewFeeEngineClient(conn), conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initLedgerClient(cfg clientConfig) ledgerclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := ledgerclient.New(ctx, ledgerclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Insecure: cfg.InsecureTransport,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to connect to ledger service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("Connected to ledger service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initGatewayClient(cfg clientConfig) chainclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := chainclient.New(ctx, chainclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Insecure: cfg.InsecureTransport,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("failed to connect to chain gateway service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("connected to chain gateway service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initMntxClient(cfg clientConfig) mntxclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := mntxclient.New(ctx, mntxclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Logger: i.logger.Named("client.mntx"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to connect to mntx gateway service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("Connected to mntx gateway service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initOracleClient(cfg clientConfig) oracleclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := oracleclient.New(ctx, oracleclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Insecure: cfg.InsecureTransport,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to connect to oracle service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("Connected to oracle service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) loadConfig() (*config, error) {
|
|
||||||
data, err := os.ReadFile(i.file)
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &config{Config: &grpcapp.Config{}}
|
|
||||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
|
||||||
i.logger.Error("Failed to parse configuration", zap.Error(err))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Runtime == nil {
|
|
||||||
cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.GRPC == nil {
|
|
||||||
cfg.GRPC = &routers.GRPCConfig{
|
|
||||||
Network: "tcp",
|
|
||||||
Address: ":50062",
|
|
||||||
EnableReflection: true,
|
|
||||||
EnableHealth: true,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if strings.TrimSpace(cfg.GRPC.Address) == "" {
|
|
||||||
cfg.GRPC.Address = ":50062"
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(cfg.GRPC.Network) == "" {
|
|
||||||
cfg.GRPC.Network = "tcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Metrics == nil {
|
|
||||||
cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9403"}
|
|
||||||
} else if strings.TrimSpace(cfg.Metrics.Address) == "" {
|
|
||||||
cfg.Metrics.Address = ":9403"
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildCardGatewayRoutes(src map[string]cardGatewayRouteConfig) map[string]orchestrator.CardGatewayRoute {
|
|
||||||
if len(src) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result := make(map[string]orchestrator.CardGatewayRoute, len(src))
|
|
||||||
for key, route := range src {
|
|
||||||
trimmedKey := strings.TrimSpace(key)
|
|
||||||
if trimmedKey == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[trimmedKey] = orchestrator.CardGatewayRoute{
|
|
||||||
FundingAddress: strings.TrimSpace(route.FundingAddress),
|
|
||||||
FeeAddress: strings.TrimSpace(route.FeeAddress),
|
|
||||||
FeeWalletRef: strings.TrimSpace(route.FeeWalletRef),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFeeLedgerAccounts(src map[string]string) map[string]string {
|
|
||||||
if len(src) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result := make(map[string]string, len(src))
|
|
||||||
for key, account := range src {
|
|
||||||
k := strings.ToLower(strings.TrimSpace(key))
|
|
||||||
v := strings.TrimSpace(account)
|
|
||||||
if k == "" || v == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[k] = v
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildGatewayRegistry(logger mlogger.Logger, mntxClient mntxclient.Client, src []gatewayInstanceConfig, registry *discovery.Registry) orchestrator.GatewayRegistry {
|
|
||||||
static := buildGatewayInstances(logger, src)
|
|
||||||
staticRegistry := orchestrator.NewGatewayRegistry(logger, mntxClient, static)
|
|
||||||
discoveryRegistry := orchestrator.NewDiscoveryGatewayRegistry(logger, registry)
|
|
||||||
return orchestrator.NewCompositeGatewayRegistry(logger, staticRegistry, discoveryRegistry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildRailGateways(chainClient chainclient.Client, src []gatewayInstanceConfig) map[string]rail.RailGateway {
|
|
||||||
if chainClient == nil || len(src) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
instances := buildGatewayInstances(nil, src)
|
|
||||||
if len(instances) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result := map[string]rail.RailGateway{}
|
|
||||||
for _, inst := range instances {
|
|
||||||
if inst == nil || !inst.IsEnabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if inst.Rail != model.RailCrypto {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfg := chainclient.RailGatewayConfig{
|
|
||||||
Rail: string(inst.Rail),
|
|
||||||
Network: inst.Network,
|
|
||||||
Capabilities: rail.RailCapabilities{
|
|
||||||
CanPayIn: inst.Capabilities.CanPayIn,
|
|
||||||
CanPayOut: inst.Capabilities.CanPayOut,
|
|
||||||
CanReadBalance: inst.Capabilities.CanReadBalance,
|
|
||||||
CanSendFee: inst.Capabilities.CanSendFee,
|
|
||||||
RequiresObserveConfirm: inst.Capabilities.RequiresObserveConfirm,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
result[inst.ID] = chainclient.NewRailGateway(chainClient, cfg)
|
|
||||||
}
|
|
||||||
if len(result) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildGatewayInstances(logger mlogger.Logger, src []gatewayInstanceConfig) []*model.GatewayInstanceDescriptor {
|
|
||||||
if len(src) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if logger != nil {
|
|
||||||
logger = logger.Named("gateway_instances")
|
|
||||||
}
|
|
||||||
result := make([]*model.GatewayInstanceDescriptor, 0, len(src))
|
|
||||||
for _, cfg := range src {
|
|
||||||
id := strings.TrimSpace(cfg.ID)
|
|
||||||
if id == "" {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("Gateway instance skipped: missing id")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rail := parseRail(cfg.Rail)
|
|
||||||
if rail == model.RailUnspecified {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("Gateway instance skipped: invalid rail", zap.String("id", id), zap.String("rail", cfg.Rail))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
enabled := true
|
|
||||||
if cfg.IsEnabled != nil {
|
|
||||||
enabled = *cfg.IsEnabled
|
|
||||||
}
|
|
||||||
result = append(result, &model.GatewayInstanceDescriptor{
|
|
||||||
ID: id,
|
|
||||||
Rail: rail,
|
|
||||||
Network: strings.ToUpper(strings.TrimSpace(cfg.Network)),
|
|
||||||
Currencies: normalizeCurrencies(cfg.Currencies),
|
|
||||||
Capabilities: model.RailCapabilities{
|
|
||||||
CanPayIn: cfg.Capabilities.CanPayIn,
|
|
||||||
CanPayOut: cfg.Capabilities.CanPayOut,
|
|
||||||
CanReadBalance: cfg.Capabilities.CanReadBalance,
|
|
||||||
CanSendFee: cfg.Capabilities.CanSendFee,
|
|
||||||
RequiresObserveConfirm: cfg.Capabilities.RequiresObserveConfirm,
|
|
||||||
},
|
|
||||||
Limits: buildGatewayLimits(cfg.Limits),
|
|
||||||
Version: strings.TrimSpace(cfg.Version),
|
|
||||||
IsEnabled: enabled,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRail(value string) model.Rail {
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
|
||||||
case string(model.RailCrypto):
|
|
||||||
return model.RailCrypto
|
|
||||||
case string(model.RailProviderSettlement):
|
|
||||||
return model.RailProviderSettlement
|
|
||||||
case string(model.RailLedger):
|
|
||||||
return model.RailLedger
|
|
||||||
case string(model.RailCardPayout):
|
|
||||||
return model.RailCardPayout
|
|
||||||
case string(model.RailFiatOnRamp):
|
|
||||||
return model.RailFiatOnRamp
|
|
||||||
default:
|
|
||||||
return model.RailUnspecified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeCurrencies(values []string) []string {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seen := map[string]bool{}
|
|
||||||
result := make([]string, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
clean := strings.ToUpper(strings.TrimSpace(value))
|
|
||||||
if clean == "" || seen[clean] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[clean] = true
|
|
||||||
result = append(result, clean)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildGatewayLimits(cfg limitsConfig) model.Limits {
|
|
||||||
limits := model.Limits{
|
|
||||||
MinAmount: strings.TrimSpace(cfg.MinAmount),
|
|
||||||
MaxAmount: strings.TrimSpace(cfg.MaxAmount),
|
|
||||||
PerTxMaxFee: strings.TrimSpace(cfg.PerTxMaxFee),
|
|
||||||
PerTxMinAmount: strings.TrimSpace(cfg.PerTxMinAmount),
|
|
||||||
PerTxMaxAmount: strings.TrimSpace(cfg.PerTxMaxAmount),
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.VolumeLimit) > 0 {
|
|
||||||
limits.VolumeLimit = map[string]string{}
|
|
||||||
for key, value := range cfg.VolumeLimit {
|
|
||||||
bucket := strings.TrimSpace(key)
|
|
||||||
amount := strings.TrimSpace(value)
|
|
||||||
if bucket == "" || amount == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
limits.VolumeLimit[bucket] = amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.VelocityLimit) > 0 {
|
|
||||||
limits.VelocityLimit = map[string]int{}
|
|
||||||
for key, value := range cfg.VelocityLimit {
|
|
||||||
bucket := strings.TrimSpace(key)
|
|
||||||
if bucket == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
limits.VelocityLimit[bucket] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.CurrencyLimits) > 0 {
|
|
||||||
limits.CurrencyLimits = map[string]model.LimitsOverride{}
|
|
||||||
for key, override := range cfg.CurrencyLimits {
|
|
||||||
currency := strings.ToUpper(strings.TrimSpace(key))
|
|
||||||
if currency == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
limits.CurrencyLimits[currency] = model.LimitsOverride{
|
|
||||||
MaxVolume: strings.TrimSpace(override.MaxVolume),
|
|
||||||
MinAmount: strings.TrimSpace(override.MinAmount),
|
|
||||||
MaxAmount: strings.TrimSpace(override.MaxAmount),
|
|
||||||
MaxFee: strings.TrimSpace(override.MaxFee),
|
|
||||||
MaxOps: override.MaxOps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return limits
|
|
||||||
}
|
|
||||||
|
|||||||
30
api/payments/orchestrator/internal/server/internal/types.go
Normal file
30
api/payments/orchestrator/internal/server/internal/types.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||||
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
|
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||||
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
|
"github.com/tech/sendico/payments/orchestrator/storage"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Imp struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
file string
|
||||||
|
debug bool
|
||||||
|
|
||||||
|
config *config
|
||||||
|
app *grpcapp.App[storage.Repository]
|
||||||
|
discoveryWatcher *discovery.RegistryWatcher
|
||||||
|
discoveryReg *discovery.Registry
|
||||||
|
discoveryAnnouncer *discovery.Announcer
|
||||||
|
feesConn *grpc.ClientConn
|
||||||
|
ledgerClient ledgerclient.Client
|
||||||
|
gatewayClient chainclient.Client
|
||||||
|
mntxClient mntxclient.Client
|
||||||
|
oracleClient oracleclient.Client
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ func (r *discoveryGatewayRegistry) List(_ context.Context) ([]*model.GatewayInst
|
|||||||
}
|
}
|
||||||
items = append(items, &model.GatewayInstanceDescriptor{
|
items = append(items, &model.GatewayInstanceDescriptor{
|
||||||
ID: entry.ID,
|
ID: entry.ID,
|
||||||
|
InstanceID: entry.InstanceID,
|
||||||
Rail: rail,
|
Rail: rail,
|
||||||
Network: entry.Network,
|
Network: entry.Network,
|
||||||
Currencies: normalizeCurrencies(entry.Currencies),
|
Currencies: normalizeCurrencies(entry.Currencies),
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ type Limits struct {
|
|||||||
// GatewayInstanceDescriptor standardizes gateway instance self-declaration.
|
// GatewayInstanceDescriptor standardizes gateway instance self-declaration.
|
||||||
type GatewayInstanceDescriptor struct {
|
type GatewayInstanceDescriptor struct {
|
||||||
ID string `bson:"id" json:"id"`
|
ID string `bson:"id" json:"id"`
|
||||||
|
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
|
||||||
Rail Rail `bson:"rail" json:"rail"`
|
Rail Rail `bson:"rail" json:"rail"`
|
||||||
Network string `bson:"network,omitempty" json:"network,omitempty"`
|
Network string `bson:"network,omitempty" json:"network,omitempty"`
|
||||||
Currencies []string `bson:"currencies,omitempty" json:"currencies,omitempty"`
|
Currencies []string `bson:"currencies,omitempty" json:"currencies,omitempty"`
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Announcer struct {
|
type Announcer struct {
|
||||||
@@ -31,8 +32,11 @@ func NewAnnouncer(logger mlogger.Logger, producer msg.Producer, sender string, a
|
|||||||
if announce.Service == "" {
|
if announce.Service == "" {
|
||||||
announce.Service = strings.TrimSpace(sender)
|
announce.Service = strings.TrimSpace(sender)
|
||||||
}
|
}
|
||||||
|
if announce.InstanceID == "" {
|
||||||
|
announce.InstanceID = InstanceID()
|
||||||
|
}
|
||||||
if announce.ID == "" {
|
if announce.ID == "" {
|
||||||
announce.ID = DefaultInstanceID(announce.Service)
|
announce.ID = DefaultEntryID(announce.Service)
|
||||||
}
|
}
|
||||||
if announce.InvokeURI == "" && announce.Service != "" {
|
if announce.InvokeURI == "" && announce.Service != "" {
|
||||||
announce.InvokeURI = DefaultInvokeURI(announce.Service)
|
announce.InvokeURI = DefaultInvokeURI(announce.Service)
|
||||||
@@ -53,15 +57,16 @@ func (a *Announcer) Start() {
|
|||||||
}
|
}
|
||||||
a.startOnce.Do(func() {
|
a.startOnce.Do(func() {
|
||||||
if a.producer == nil {
|
if a.producer == nil {
|
||||||
a.logWarn("Discovery announce skipped: producer not configured")
|
a.logWarn("Discovery announce skipped: producer not configured", announcementFields(a.announce)...)
|
||||||
close(a.doneCh)
|
close(a.doneCh)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(a.announce.ID) == "" {
|
if strings.TrimSpace(a.announce.ID) == "" {
|
||||||
a.logWarn("Discovery announce skipped: missing instance id")
|
a.logWarn("Discovery announce skipped: missing instance id", announcementFields(a.announce)...)
|
||||||
close(a.doneCh)
|
close(a.doneCh)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
a.logInfo("Discovery announcer starting", announcementFields(a.announce)...)
|
||||||
a.sendAnnouncement()
|
a.sendAnnouncement()
|
||||||
a.sendHeartbeat()
|
a.sendHeartbeat()
|
||||||
go a.heartbeatLoop()
|
go a.heartbeatLoop()
|
||||||
@@ -75,6 +80,7 @@ func (a *Announcer) Stop() {
|
|||||||
a.stopOnce.Do(func() {
|
a.stopOnce.Do(func() {
|
||||||
close(a.stopCh)
|
close(a.stopCh)
|
||||||
<-a.doneCh
|
<-a.doneCh
|
||||||
|
a.logInfo("Discovery announcer stopped", announcementFields(a.announce)...)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,42 +105,47 @@ func (a *Announcer) heartbeatLoop() {
|
|||||||
|
|
||||||
func (a *Announcer) sendAnnouncement() {
|
func (a *Announcer) sendAnnouncement() {
|
||||||
env := NewServiceAnnounceEnvelope(a.sender, a.announce)
|
env := NewServiceAnnounceEnvelope(a.sender, a.announce)
|
||||||
|
event := ServiceAnnounceEvent()
|
||||||
if a.announce.Rail != "" {
|
if a.announce.Rail != "" {
|
||||||
env = NewGatewayAnnounceEnvelope(a.sender, a.announce)
|
env = NewGatewayAnnounceEnvelope(a.sender, a.announce)
|
||||||
|
event = GatewayAnnounceEvent()
|
||||||
}
|
}
|
||||||
if err := a.producer.SendMessage(env); err != nil {
|
if err := a.producer.SendMessage(env); err != nil {
|
||||||
a.logWarn("Failed to publish discovery announce: " + err.Error())
|
fields := append(announcementFields(a.announce), zap.String("event", event.ToString()), zap.Error(err))
|
||||||
|
a.logWarn("Failed to publish discovery announce", fields...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.logInfo("Discovery announce published")
|
a.logInfo("Discovery announce published", append(announcementFields(a.announce), zap.String("event", event.ToString()))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announcer) sendHeartbeat() {
|
func (a *Announcer) sendHeartbeat() {
|
||||||
hb := Heartbeat{
|
hb := Heartbeat{
|
||||||
ID: a.announce.ID,
|
ID: a.announce.ID,
|
||||||
Status: "ok",
|
InstanceID: a.announce.InstanceID,
|
||||||
TS: time.Now().Unix(),
|
Status: "ok",
|
||||||
|
TS: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
if err := a.producer.SendMessage(NewHeartbeatEnvelope(a.sender, hb)); err != nil {
|
if err := a.producer.SendMessage(NewHeartbeatEnvelope(a.sender, hb)); err != nil {
|
||||||
a.logWarn("Failed to publish discovery heartbeat: " + err.Error())
|
fields := append(announcementFields(a.announce), zap.String("event", HeartbeatEvent().ToString()), zap.Error(err))
|
||||||
|
a.logWarn("Failed to publish discovery heartbeat", fields...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announcer) logInfo(message string) {
|
func (a *Announcer) logInfo(message string, fields ...zap.Field) {
|
||||||
if a.logger == nil {
|
if a.logger == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.logger.Info(message)
|
a.logger.Info(message, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announcer) logWarn(message string) {
|
func (a *Announcer) logWarn(message string, fields ...zap.Field) {
|
||||||
if a.logger == nil {
|
if a.logger == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.logger.Warn(message)
|
a.logger.Warn(message, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultInstanceID(service string) string {
|
func DefaultEntryID(service string) string {
|
||||||
clean := strings.ToLower(strings.TrimSpace(service))
|
clean := strings.ToLower(strings.TrimSpace(service))
|
||||||
if clean == "" {
|
if clean == "" {
|
||||||
clean = "service"
|
clean = "service"
|
||||||
@@ -148,6 +159,10 @@ func DefaultInstanceID(service string) string {
|
|||||||
return clean + "_" + host + "_" + uid
|
return clean + "_" + host + "_" + uid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DefaultInstanceID(service string) string {
|
||||||
|
return DefaultEntryID(service)
|
||||||
|
}
|
||||||
|
|
||||||
func DefaultInvokeURI(service string) string {
|
func DefaultInvokeURI(service string) string {
|
||||||
clean := strings.ToLower(strings.TrimSpace(service))
|
clean := strings.ToLower(strings.TrimSpace(service))
|
||||||
if clean == "" {
|
if clean == "" {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/messaging/broker"
|
mb "github.com/tech/sendico/pkg/messaging/broker"
|
||||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||||
me "github.com/tech/sendico/pkg/messaging/envelope"
|
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
|
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
|
||||||
@@ -27,22 +27,22 @@ type Client struct {
|
|||||||
pending map[string]chan LookupResponse
|
pending map[string]chan LookupResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(logger mlogger.Logger, broker broker.Broker, producer msg.Producer, sender string) (*Client, error) {
|
func NewClient(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, sender string) (*Client, error) {
|
||||||
if broker == nil {
|
if msgBroker == nil {
|
||||||
return nil, errors.New("discovery client: broker is nil")
|
return nil, errors.New("discovery client: broker is nil")
|
||||||
}
|
}
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
logger = logger.Named("discovery_client")
|
logger = logger.Named("discovery_client")
|
||||||
}
|
}
|
||||||
if producer == nil {
|
if producer == nil {
|
||||||
producer = msgproducer.NewProducer(logger, broker)
|
producer = msgproducer.NewProducer(logger, msgBroker)
|
||||||
}
|
}
|
||||||
sender = strings.TrimSpace(sender)
|
sender = strings.TrimSpace(sender)
|
||||||
if sender == "" {
|
if sender == "" {
|
||||||
sender = "discovery_client"
|
sender = "discovery_client"
|
||||||
}
|
}
|
||||||
|
|
||||||
consumer, err := cons.NewConsumer(logger, broker, LookupResponseEvent())
|
consumer, err := cons.NewConsumer(logger, msgBroker, LookupResponseEvent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func NewClient(logger mlogger.Logger, broker broker.Broker, producer msg.Produce
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := consumer.ConsumeMessages(client.handleLookupResponse); err != nil && client.logger != nil {
|
if err := consumer.ConsumeMessages(client.handleLookupResponse); err != nil && client.logger != nil {
|
||||||
client.logger.Warn("Discovery lookup consumer stopped", zap.Error(err))
|
client.logger.Warn("Discovery lookup consumer stopped", zap.String("event", LookupResponseEvent().ToString()), zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -112,7 +112,8 @@ func (c *Client) Lookup(ctx context.Context) (LookupResponse, error) {
|
|||||||
func (c *Client) handleLookupResponse(_ context.Context, env me.Envelope) error {
|
func (c *Client) handleLookupResponse(_ context.Context, env me.Envelope) error {
|
||||||
var payload LookupResponse
|
var payload LookupResponse
|
||||||
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
|
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
|
||||||
c.logWarn("Failed to decode discovery lookup response", zap.Error(err))
|
fields := append(envelopeFields(env), zap.Int("data_len", len(env.GetData())), zap.Error(err))
|
||||||
|
c.logWarn("Failed to decode discovery lookup response", fields...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
requestID := strings.TrimSpace(payload.RequestID)
|
requestID := strings.TrimSpace(payload.RequestID)
|
||||||
|
|||||||
27
api/pkg/discovery/instanceid.go
Normal file
27
api/pkg/discovery/instanceid.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
instanceID string
|
||||||
|
instanceOnce sync.Once
|
||||||
|
instanceIDGenerator = func() string {
|
||||||
|
return uuid.NewString()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstanceID returns a unique, process-stable identifier for the running service instance.
|
||||||
|
func InstanceID() string {
|
||||||
|
instanceOnce.Do(func() {
|
||||||
|
instanceID = strings.TrimSpace(instanceIDGenerator())
|
||||||
|
if instanceID == "" {
|
||||||
|
instanceID = uuid.NewString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return instanceID
|
||||||
|
}
|
||||||
53
api/pkg/discovery/instanceid_test.go
Normal file
53
api/pkg/discovery/instanceid_test.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resetInstanceIDForTest() {
|
||||||
|
instanceID = ""
|
||||||
|
instanceOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstanceIDStable(t *testing.T) {
|
||||||
|
resetInstanceIDForTest()
|
||||||
|
original := instanceIDGenerator
|
||||||
|
defer func() {
|
||||||
|
instanceIDGenerator = original
|
||||||
|
resetInstanceIDForTest()
|
||||||
|
}()
|
||||||
|
|
||||||
|
instanceIDGenerator = func() string {
|
||||||
|
return "fixed-id"
|
||||||
|
}
|
||||||
|
|
||||||
|
first := InstanceID()
|
||||||
|
second := InstanceID()
|
||||||
|
if first != "fixed-id" || second != "fixed-id" {
|
||||||
|
t.Fatalf("expected stable instance id, got %q and %q", first, second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstanceIDRegeneratesAfterReset(t *testing.T) {
|
||||||
|
resetInstanceIDForTest()
|
||||||
|
original := instanceIDGenerator
|
||||||
|
defer func() {
|
||||||
|
instanceIDGenerator = original
|
||||||
|
resetInstanceIDForTest()
|
||||||
|
}()
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
instanceIDGenerator = func() string {
|
||||||
|
counter++
|
||||||
|
return fmt.Sprintf("id-%d", counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
first := InstanceID()
|
||||||
|
resetInstanceIDForTest()
|
||||||
|
second := InstanceID()
|
||||||
|
if first == second {
|
||||||
|
t.Fatalf("expected new instance id after reset, got %q", first)
|
||||||
|
}
|
||||||
|
}
|
||||||
99
api/pkg/discovery/keys.go
Normal file
99
api/pkg/discovery/keys.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const kvEntryPrefix = "entry."
|
||||||
|
|
||||||
|
func registryEntryKey(entry RegistryEntry) string {
|
||||||
|
return registryKey(entry.Service, entry.Rail, entry.Network, entry.Operations, entry.Version, entry.InstanceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registryKey(service, rail, network string, operations []string, version, instanceID string) string {
|
||||||
|
service = normalizeKeyPart(service)
|
||||||
|
rail = normalizeKeyPart(rail)
|
||||||
|
op := normalizeKeyPart(firstOperation(operations))
|
||||||
|
version = normalizeKeyPart(version)
|
||||||
|
instanceID = normalizeKeyPart(instanceID)
|
||||||
|
if instanceID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if service == "" {
|
||||||
|
service = "service"
|
||||||
|
}
|
||||||
|
if rail == "" {
|
||||||
|
rail = "none"
|
||||||
|
}
|
||||||
|
if op == "" {
|
||||||
|
op = "none"
|
||||||
|
}
|
||||||
|
if version == "" {
|
||||||
|
version = "unknown"
|
||||||
|
}
|
||||||
|
parts := []string{service, rail, op, version, instanceID}
|
||||||
|
if network != "" {
|
||||||
|
netPart := normalizeKeyPart(network)
|
||||||
|
if netPart != "" {
|
||||||
|
parts = append(parts, netPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func kvKeyFromRegistryKey(key string) string {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(key, kvEntryPrefix) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return kvEntryPrefix + key
|
||||||
|
}
|
||||||
|
|
||||||
|
func registryKeyFromKVKey(key string) string {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if strings.HasPrefix(key, kvEntryPrefix) {
|
||||||
|
return strings.TrimPrefix(key, kvEntryPrefix)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstOperation(ops []string) string {
|
||||||
|
for _, op := range ops {
|
||||||
|
op = strings.TrimSpace(op)
|
||||||
|
if op != "" {
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeKeyPart(value string) string {
|
||||||
|
value = strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(value))
|
||||||
|
lastDash := false
|
||||||
|
for _, r := range value {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||||
|
b.WriteRune(r)
|
||||||
|
lastDash = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r == '-' || r == '_' {
|
||||||
|
if !lastDash {
|
||||||
|
b.WriteByte('-')
|
||||||
|
lastDash = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !lastDash {
|
||||||
|
b.WriteByte('-')
|
||||||
|
lastDash = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := strings.Trim(b.String(), "-")
|
||||||
|
return out
|
||||||
|
}
|
||||||
103
api/pkg/discovery/kv.go
Normal file
103
api/pkg/discovery/kv.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultKVBucket = "discovery_registry"
|
||||||
|
|
||||||
|
type KVStore struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
kv nats.KeyValue
|
||||||
|
bucket string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKVStore(logger mlogger.Logger, js nats.JetStreamContext, bucket string) (*KVStore, error) {
|
||||||
|
if js == nil {
|
||||||
|
return nil, errors.New("discovery kv: jetstream is nil")
|
||||||
|
}
|
||||||
|
if logger != nil {
|
||||||
|
logger = logger.Named("discovery_kv")
|
||||||
|
}
|
||||||
|
bucket = strings.TrimSpace(bucket)
|
||||||
|
if bucket == "" {
|
||||||
|
bucket = DefaultKVBucket
|
||||||
|
}
|
||||||
|
kv, err := js.KeyValue(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, nats.ErrBucketNotFound) {
|
||||||
|
kv, err = js.CreateKeyValue(&nats.KeyValueConfig{
|
||||||
|
Bucket: bucket,
|
||||||
|
Description: "service discovery registry",
|
||||||
|
History: 1,
|
||||||
|
})
|
||||||
|
if err == nil && logger != nil {
|
||||||
|
logger.Info("Discovery KV bucket created", zap.String("bucket", bucket))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &KVStore{
|
||||||
|
logger: logger,
|
||||||
|
kv: kv,
|
||||||
|
bucket: bucket,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *KVStore) Put(entry RegistryEntry) error {
|
||||||
|
if s == nil || s.kv == nil {
|
||||||
|
return errors.New("discovery kv: not configured")
|
||||||
|
}
|
||||||
|
key := registryEntryKey(normalizeEntry(entry))
|
||||||
|
if key == "" {
|
||||||
|
return errors.New("discovery kv: entry key is empty")
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = s.kv.Put(kvKeyFromRegistryKey(key), payload)
|
||||||
|
if err != nil && s.logger != nil {
|
||||||
|
fields := append(entryFields(entry), zap.String("bucket", s.bucket), zap.String("key", key), zap.Error(err))
|
||||||
|
s.logger.Warn("Failed to persist discovery entry", fields...)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *KVStore) Delete(id string) error {
|
||||||
|
if s == nil || s.kv == nil {
|
||||||
|
return errors.New("discovery kv: not configured")
|
||||||
|
}
|
||||||
|
key := kvKeyFromRegistryKey(id)
|
||||||
|
if key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := s.kv.Delete(key); err != nil && s.logger != nil {
|
||||||
|
s.logger.Warn("Failed to delete discovery entry", zap.String("bucket", s.bucket), zap.String("key", key), zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *KVStore) WatchAll() (nats.KeyWatcher, error) {
|
||||||
|
if s == nil || s.kv == nil {
|
||||||
|
return nil, errors.New("discovery kv: not configured")
|
||||||
|
}
|
||||||
|
return s.kv.WatchAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *KVStore) Bucket() string {
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.bucket
|
||||||
|
}
|
||||||
108
api/pkg/discovery/logging.go
Normal file
108
api/pkg/discovery/logging.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func announcementFields(announce Announcement) []zap.Field {
|
||||||
|
fields := make([]zap.Field, 0, 10)
|
||||||
|
if announce.ID != "" {
|
||||||
|
fields = append(fields, zap.String("id", announce.ID))
|
||||||
|
}
|
||||||
|
if announce.InstanceID != "" {
|
||||||
|
fields = append(fields, zap.String("instance_id", announce.InstanceID))
|
||||||
|
}
|
||||||
|
if announce.Service != "" {
|
||||||
|
fields = append(fields, zap.String("service", announce.Service))
|
||||||
|
}
|
||||||
|
if announce.Rail != "" {
|
||||||
|
fields = append(fields, zap.String("rail", announce.Rail))
|
||||||
|
}
|
||||||
|
if announce.Network != "" {
|
||||||
|
fields = append(fields, zap.String("network", announce.Network))
|
||||||
|
}
|
||||||
|
if announce.InvokeURI != "" {
|
||||||
|
fields = append(fields, zap.String("invoke_uri", announce.InvokeURI))
|
||||||
|
}
|
||||||
|
if announce.Version != "" {
|
||||||
|
fields = append(fields, zap.String("version", announce.Version))
|
||||||
|
}
|
||||||
|
if announce.RoutingPriority != 0 {
|
||||||
|
fields = append(fields, zap.Int("routing_priority", announce.RoutingPriority))
|
||||||
|
}
|
||||||
|
if len(announce.Operations) > 0 {
|
||||||
|
fields = append(fields, zap.Int("ops", len(announce.Operations)))
|
||||||
|
}
|
||||||
|
if len(announce.Currencies) > 0 {
|
||||||
|
fields = append(fields, zap.Int("currencies", len(announce.Currencies)))
|
||||||
|
}
|
||||||
|
if announce.Health.IntervalSec > 0 {
|
||||||
|
fields = append(fields, zap.Int("interval_sec", announce.Health.IntervalSec))
|
||||||
|
}
|
||||||
|
if announce.Health.TimeoutSec > 0 {
|
||||||
|
fields = append(fields, zap.Int("timeout_sec", announce.Health.TimeoutSec))
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryFields(entry RegistryEntry) []zap.Field {
|
||||||
|
fields := make([]zap.Field, 0, 12)
|
||||||
|
if entry.ID != "" {
|
||||||
|
fields = append(fields, zap.String("id", entry.ID))
|
||||||
|
}
|
||||||
|
if entry.InstanceID != "" {
|
||||||
|
fields = append(fields, zap.String("instance_id", entry.InstanceID))
|
||||||
|
}
|
||||||
|
if entry.Service != "" {
|
||||||
|
fields = append(fields, zap.String("service", entry.Service))
|
||||||
|
}
|
||||||
|
if entry.Rail != "" {
|
||||||
|
fields = append(fields, zap.String("rail", entry.Rail))
|
||||||
|
}
|
||||||
|
if entry.Network != "" {
|
||||||
|
fields = append(fields, zap.String("network", entry.Network))
|
||||||
|
}
|
||||||
|
if entry.Version != "" {
|
||||||
|
fields = append(fields, zap.String("version", entry.Version))
|
||||||
|
}
|
||||||
|
if entry.InvokeURI != "" {
|
||||||
|
fields = append(fields, zap.String("invoke_uri", entry.InvokeURI))
|
||||||
|
}
|
||||||
|
if entry.Status != "" {
|
||||||
|
fields = append(fields, zap.String("status", entry.Status))
|
||||||
|
}
|
||||||
|
if !entry.LastHeartbeat.IsZero() {
|
||||||
|
fields = append(fields, zap.Time("last_heartbeat", entry.LastHeartbeat))
|
||||||
|
}
|
||||||
|
fields = append(fields, zap.Bool("healthy", entry.Healthy))
|
||||||
|
if entry.RoutingPriority != 0 {
|
||||||
|
fields = append(fields, zap.Int("routing_priority", entry.RoutingPriority))
|
||||||
|
}
|
||||||
|
if len(entry.Operations) > 0 {
|
||||||
|
fields = append(fields, zap.Int("ops", len(entry.Operations)))
|
||||||
|
}
|
||||||
|
if len(entry.Currencies) > 0 {
|
||||||
|
fields = append(fields, zap.Int("currencies", len(entry.Currencies)))
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func envelopeFields(env me.Envelope) []zap.Field {
|
||||||
|
if env == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]zap.Field, 0, 4)
|
||||||
|
sender := strings.TrimSpace(env.GetSender())
|
||||||
|
if sender != "" {
|
||||||
|
fields = append(fields, zap.String("sender", sender))
|
||||||
|
}
|
||||||
|
if signature := env.GetSignature(); signature != nil {
|
||||||
|
fields = append(fields, zap.String("event", signature.ToString()))
|
||||||
|
}
|
||||||
|
fields = append(fields, zap.String("message_id", env.GetMessageId().String()))
|
||||||
|
fields = append(fields, zap.Time("timestamp", env.GetTimeStamp()))
|
||||||
|
return fields
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package discovery
|
package discovery
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type LookupRequest struct {
|
type LookupRequest struct {
|
||||||
RequestID string `json:"requestId,omitempty"`
|
RequestID string `json:"requestId,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -11,16 +13,18 @@ type LookupResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServiceSummary struct {
|
type ServiceSummary struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Service string `json:"service"`
|
InstanceID string `json:"instanceId"`
|
||||||
Ops []string `json:"ops,omitempty"`
|
Service string `json:"service"`
|
||||||
Version string `json:"version,omitempty"`
|
Ops []string `json:"ops,omitempty"`
|
||||||
Healthy bool `json:"healthy,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
InvokeURI string `json:"invokeURI,omitempty"`
|
Healthy bool `json:"healthy,omitempty"`
|
||||||
|
InvokeURI string `json:"invokeURI,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GatewaySummary struct {
|
type GatewaySummary struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
InstanceID string `json:"instanceId"`
|
||||||
Rail string `json:"rail"`
|
Rail string `json:"rail"`
|
||||||
Network string `json:"network,omitempty"`
|
Network string `json:"network,omitempty"`
|
||||||
Currencies []string `json:"currencies,omitempty"`
|
Currencies []string `json:"currencies,omitempty"`
|
||||||
@@ -43,6 +47,7 @@ func (r *Registry) Lookup(now time.Time) LookupResponse {
|
|||||||
if entry.Rail != "" {
|
if entry.Rail != "" {
|
||||||
resp.Gateways = append(resp.Gateways, GatewaySummary{
|
resp.Gateways = append(resp.Gateways, GatewaySummary{
|
||||||
ID: entry.ID,
|
ID: entry.ID,
|
||||||
|
InstanceID: entry.InstanceID,
|
||||||
Rail: entry.Rail,
|
Rail: entry.Rail,
|
||||||
Network: entry.Network,
|
Network: entry.Network,
|
||||||
Currencies: cloneStrings(entry.Currencies),
|
Currencies: cloneStrings(entry.Currencies),
|
||||||
@@ -56,12 +61,13 @@ func (r *Registry) Lookup(now time.Time) LookupResponse {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
resp.Services = append(resp.Services, ServiceSummary{
|
resp.Services = append(resp.Services, ServiceSummary{
|
||||||
ID: entry.ID,
|
ID: entry.ID,
|
||||||
Service: entry.Service,
|
InstanceID: entry.InstanceID,
|
||||||
Ops: cloneStrings(entry.Operations),
|
Service: entry.Service,
|
||||||
Version: entry.Version,
|
Ops: cloneStrings(entry.Operations),
|
||||||
Healthy: entry.Healthy,
|
Version: entry.Version,
|
||||||
InvokeURI: entry.InvokeURI,
|
Healthy: entry.Healthy,
|
||||||
|
InvokeURI: entry.InvokeURI,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const (
|
|||||||
|
|
||||||
type RegistryEntry struct {
|
type RegistryEntry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
InstanceID string `bson:"instanceId" json:"instanceId"`
|
||||||
Service string `json:"service"`
|
Service string `json:"service"`
|
||||||
Rail string `json:"rail,omitempty"`
|
Rail string `json:"rail,omitempty"`
|
||||||
Network string `json:"network,omitempty"`
|
Network string `json:"network,omitempty"`
|
||||||
@@ -29,8 +30,10 @@ type RegistryEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
entries map[string]*RegistryEntry
|
entries map[string]*RegistryEntry
|
||||||
|
byID map[string]map[string]struct{}
|
||||||
|
byInstance map[string]map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateResult struct {
|
type UpdateResult struct {
|
||||||
@@ -42,23 +45,31 @@ type UpdateResult struct {
|
|||||||
|
|
||||||
func NewRegistry() *Registry {
|
func NewRegistry() *Registry {
|
||||||
return &Registry{
|
return &Registry{
|
||||||
entries: map[string]*RegistryEntry{},
|
entries: map[string]*RegistryEntry{},
|
||||||
|
byID: map[string]map[string]struct{}{},
|
||||||
|
byInstance: map[string]map[string]struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) UpsertFromAnnouncement(announce Announcement, now time.Time) UpdateResult {
|
func (r *Registry) UpsertFromAnnouncement(announce Announcement, now time.Time) UpdateResult {
|
||||||
entry := registryEntryFromAnnouncement(normalizeAnnouncement(announce), now)
|
entry := registryEntryFromAnnouncement(normalizeAnnouncement(announce), now)
|
||||||
|
key := registryEntryKey(entry)
|
||||||
|
if key == "" {
|
||||||
|
return UpdateResult{Entry: entry}
|
||||||
|
}
|
||||||
|
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
existing, ok := r.entries[entry.ID]
|
existing, ok := r.entries[key]
|
||||||
wasHealthy := false
|
wasHealthy := false
|
||||||
if ok && existing != nil {
|
if ok && existing != nil {
|
||||||
wasHealthy = existing.isHealthyAt(now)
|
wasHealthy = existing.isHealthyAt(now)
|
||||||
|
r.unindexEntry(key, existing)
|
||||||
}
|
}
|
||||||
entry.Healthy = entry.isHealthyAt(now)
|
entry.Healthy = entry.isHealthyAt(now)
|
||||||
r.entries[entry.ID] = &entry
|
r.entries[key] = &entry
|
||||||
|
r.indexEntry(key, &entry)
|
||||||
|
|
||||||
return UpdateResult{
|
return UpdateResult{
|
||||||
Entry: entry,
|
Entry: entry,
|
||||||
@@ -68,10 +79,45 @@ func (r *Registry) UpsertFromAnnouncement(announce Announcement, now time.Time)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) UpdateHeartbeat(id string, status string, ts time.Time, now time.Time) (UpdateResult, bool) {
|
func (r *Registry) UpsertEntry(entry RegistryEntry, now time.Time) UpdateResult {
|
||||||
|
entry = normalizeEntry(entry)
|
||||||
|
key := registryEntryKey(entry)
|
||||||
|
if key == "" {
|
||||||
|
return UpdateResult{Entry: entry}
|
||||||
|
}
|
||||||
|
if entry.LastHeartbeat.IsZero() {
|
||||||
|
entry.LastHeartbeat = now
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(entry.Status) == "" {
|
||||||
|
entry.Status = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
existing, ok := r.entries[key]
|
||||||
|
wasHealthy := false
|
||||||
|
if ok && existing != nil {
|
||||||
|
wasHealthy = existing.isHealthyAt(now)
|
||||||
|
r.unindexEntry(key, existing)
|
||||||
|
}
|
||||||
|
entry.Healthy = entry.isHealthyAt(now)
|
||||||
|
r.entries[key] = &entry
|
||||||
|
r.indexEntry(key, &entry)
|
||||||
|
|
||||||
|
return UpdateResult{
|
||||||
|
Entry: entry,
|
||||||
|
IsNew: !ok,
|
||||||
|
WasHealthy: wasHealthy,
|
||||||
|
BecameHealthy: !wasHealthy && entry.Healthy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) UpdateHeartbeat(id string, instanceID string, status string, ts time.Time, now time.Time) []UpdateResult {
|
||||||
id = strings.TrimSpace(id)
|
id = strings.TrimSpace(id)
|
||||||
if id == "" {
|
instanceID = strings.TrimSpace(instanceID)
|
||||||
return UpdateResult{}, false
|
if id == "" && instanceID == "" {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if status == "" {
|
if status == "" {
|
||||||
status = "ok"
|
status = "ok"
|
||||||
@@ -83,21 +129,54 @@ func (r *Registry) UpdateHeartbeat(id string, status string, ts time.Time, now t
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
entry, ok := r.entries[id]
|
keys := keysFromIndex(r.byInstance[instanceID])
|
||||||
if !ok || entry == nil {
|
if len(keys) == 0 && id != "" {
|
||||||
return UpdateResult{}, false
|
keys = keysFromIndex(r.byID[id])
|
||||||
}
|
}
|
||||||
wasHealthy := entry.isHealthyAt(now)
|
if len(keys) == 0 {
|
||||||
entry.Status = status
|
return nil
|
||||||
entry.LastHeartbeat = ts
|
}
|
||||||
entry.Healthy = entry.isHealthyAt(now)
|
results := make([]UpdateResult, 0, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
entry := r.entries[key]
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if id != "" && entry.ID != id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if instanceID != "" && entry.InstanceID != instanceID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wasHealthy := entry.isHealthyAt(now)
|
||||||
|
entry.Status = status
|
||||||
|
entry.LastHeartbeat = ts
|
||||||
|
entry.Healthy = entry.isHealthyAt(now)
|
||||||
|
|
||||||
return UpdateResult{
|
results = append(results, UpdateResult{
|
||||||
Entry: *entry,
|
Entry: *entry,
|
||||||
IsNew: false,
|
IsNew: false,
|
||||||
WasHealthy: wasHealthy,
|
WasHealthy: wasHealthy,
|
||||||
BecameHealthy: !wasHealthy && entry.Healthy,
|
BecameHealthy: !wasHealthy && entry.Healthy,
|
||||||
}, true
|
})
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Delete(key string) bool {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
entry, ok := r.entries[key]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
delete(r.entries, key)
|
||||||
|
r.unindexEntry(key, entry)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) List(now time.Time, onlyHealthy bool) []RegistryEntry {
|
func (r *Registry) List(now time.Time, onlyHealthy bool) []RegistryEntry {
|
||||||
@@ -123,6 +202,7 @@ func registryEntryFromAnnouncement(announce Announcement, now time.Time) Registr
|
|||||||
status := "ok"
|
status := "ok"
|
||||||
return RegistryEntry{
|
return RegistryEntry{
|
||||||
ID: strings.TrimSpace(announce.ID),
|
ID: strings.TrimSpace(announce.ID),
|
||||||
|
InstanceID: strings.TrimSpace(announce.InstanceID),
|
||||||
Service: strings.TrimSpace(announce.Service),
|
Service: strings.TrimSpace(announce.Service),
|
||||||
Rail: strings.ToUpper(strings.TrimSpace(announce.Rail)),
|
Rail: strings.ToUpper(strings.TrimSpace(announce.Rail)),
|
||||||
Network: strings.ToUpper(strings.TrimSpace(announce.Network)),
|
Network: strings.ToUpper(strings.TrimSpace(announce.Network)),
|
||||||
@@ -138,8 +218,33 @@ func registryEntryFromAnnouncement(announce Announcement, now time.Time) Registr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeEntry(entry RegistryEntry) RegistryEntry {
|
||||||
|
entry.ID = strings.TrimSpace(entry.ID)
|
||||||
|
entry.InstanceID = strings.TrimSpace(entry.InstanceID)
|
||||||
|
if entry.InstanceID == "" {
|
||||||
|
entry.InstanceID = entry.ID
|
||||||
|
}
|
||||||
|
entry.Service = strings.TrimSpace(entry.Service)
|
||||||
|
entry.Rail = strings.ToUpper(strings.TrimSpace(entry.Rail))
|
||||||
|
entry.Network = strings.ToUpper(strings.TrimSpace(entry.Network))
|
||||||
|
entry.Operations = normalizeStrings(entry.Operations, false)
|
||||||
|
entry.Currencies = normalizeStrings(entry.Currencies, true)
|
||||||
|
entry.InvokeURI = strings.TrimSpace(entry.InvokeURI)
|
||||||
|
entry.Version = strings.TrimSpace(entry.Version)
|
||||||
|
entry.Status = strings.TrimSpace(entry.Status)
|
||||||
|
entry.Health = normalizeHealth(entry.Health)
|
||||||
|
if entry.Limits != nil {
|
||||||
|
entry.Limits = normalizeLimits(*entry.Limits)
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeAnnouncement(announce Announcement) Announcement {
|
func normalizeAnnouncement(announce Announcement) Announcement {
|
||||||
announce.ID = strings.TrimSpace(announce.ID)
|
announce.ID = strings.TrimSpace(announce.ID)
|
||||||
|
announce.InstanceID = strings.TrimSpace(announce.InstanceID)
|
||||||
|
if announce.InstanceID == "" {
|
||||||
|
announce.InstanceID = announce.ID
|
||||||
|
}
|
||||||
announce.Service = strings.TrimSpace(announce.Service)
|
announce.Service = strings.TrimSpace(announce.Service)
|
||||||
announce.Rail = strings.ToUpper(strings.TrimSpace(announce.Rail))
|
announce.Rail = strings.ToUpper(strings.TrimSpace(announce.Rail))
|
||||||
announce.Network = strings.ToUpper(strings.TrimSpace(announce.Network))
|
announce.Network = strings.ToUpper(strings.TrimSpace(announce.Network))
|
||||||
@@ -239,6 +344,67 @@ func cloneStrings(values []string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Registry) indexEntry(key string, entry *RegistryEntry) {
|
||||||
|
if r == nil || entry == nil || key == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if entry.ID != "" {
|
||||||
|
addIndex(r.byID, entry.ID, key)
|
||||||
|
}
|
||||||
|
if entry.InstanceID != "" {
|
||||||
|
addIndex(r.byInstance, entry.InstanceID, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) unindexEntry(key string, entry *RegistryEntry) {
|
||||||
|
if r == nil || entry == nil || key == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if entry.ID != "" {
|
||||||
|
removeIndex(r.byID, entry.ID, key)
|
||||||
|
}
|
||||||
|
if entry.InstanceID != "" {
|
||||||
|
removeIndex(r.byInstance, entry.InstanceID, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addIndex(index map[string]map[string]struct{}, id string, key string) {
|
||||||
|
if id == "" || key == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set := index[id]
|
||||||
|
if set == nil {
|
||||||
|
set = map[string]struct{}{}
|
||||||
|
index[id] = set
|
||||||
|
}
|
||||||
|
set[key] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeIndex(index map[string]map[string]struct{}, id string, key string) {
|
||||||
|
if id == "" || key == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set := index[id]
|
||||||
|
if set == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(set, key)
|
||||||
|
if len(set) == 0 {
|
||||||
|
delete(index, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keysFromIndex(index map[string]struct{}) []string {
|
||||||
|
if len(index) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(index))
|
||||||
|
for key := range index {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
func (e *RegistryEntry) isHealthyAt(now time.Time) bool {
|
func (e *RegistryEntry) isHealthyAt(now time.Time) bool {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/messaging/broker"
|
mb "github.com/tech/sendico/pkg/messaging/broker"
|
||||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||||
me "github.com/tech/sendico/pkg/messaging/envelope"
|
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
@@ -22,6 +23,8 @@ type RegistryService struct {
|
|||||||
producer msg.Producer
|
producer msg.Producer
|
||||||
sender string
|
sender string
|
||||||
consumers []consumerHandler
|
consumers []consumerHandler
|
||||||
|
kv *KVStore
|
||||||
|
kvWatcher nats.KeyWatcher
|
||||||
|
|
||||||
startOnce sync.Once
|
startOnce sync.Once
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
@@ -30,10 +33,11 @@ type RegistryService struct {
|
|||||||
type consumerHandler struct {
|
type consumerHandler struct {
|
||||||
consumer msg.Consumer
|
consumer msg.Consumer
|
||||||
handler msg.MessageHandlerT
|
handler msg.MessageHandlerT
|
||||||
|
event string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistryService(logger mlogger.Logger, broker broker.Broker, producer msg.Producer, registry *Registry, sender string) (*RegistryService, error) {
|
func NewRegistryService(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, registry *Registry, sender string) (*RegistryService, error) {
|
||||||
if broker == nil {
|
if msgBroker == nil {
|
||||||
return nil, errors.New("discovery registry: broker is nil")
|
return nil, errors.New("discovery registry: broker is nil")
|
||||||
}
|
}
|
||||||
if registry == nil {
|
if registry == nil {
|
||||||
@@ -47,19 +51,19 @@ func NewRegistryService(logger mlogger.Logger, broker broker.Broker, producer ms
|
|||||||
sender = "discovery"
|
sender = "discovery"
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceConsumer, err := cons.NewConsumer(logger, broker, ServiceAnnounceEvent())
|
serviceConsumer, err := cons.NewConsumer(logger, msgBroker, ServiceAnnounceEvent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
gatewayConsumer, err := cons.NewConsumer(logger, broker, GatewayAnnounceEvent())
|
gatewayConsumer, err := cons.NewConsumer(logger, msgBroker, GatewayAnnounceEvent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
heartbeatConsumer, err := cons.NewConsumer(logger, broker, HeartbeatEvent())
|
heartbeatConsumer, err := cons.NewConsumer(logger, msgBroker, HeartbeatEvent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
lookupConsumer, err := cons.NewConsumer(logger, broker, LookupRequestEvent())
|
lookupConsumer, err := cons.NewConsumer(logger, msgBroker, LookupRequestEvent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -69,17 +73,18 @@ func NewRegistryService(logger mlogger.Logger, broker broker.Broker, producer ms
|
|||||||
registry: registry,
|
registry: registry,
|
||||||
producer: producer,
|
producer: producer,
|
||||||
sender: sender,
|
sender: sender,
|
||||||
consumers: []consumerHandler{
|
|
||||||
{consumer: serviceConsumer, handler: func(ctx context.Context, env me.Envelope) error {
|
|
||||||
return svc.handleAnnounce(ctx, env)
|
|
||||||
}},
|
|
||||||
{consumer: gatewayConsumer, handler: func(ctx context.Context, env me.Envelope) error {
|
|
||||||
return svc.handleAnnounce(ctx, env)
|
|
||||||
}},
|
|
||||||
{consumer: heartbeatConsumer, handler: svc.handleHeartbeat},
|
|
||||||
{consumer: lookupConsumer, handler: svc.handleLookup},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
svc.consumers = []consumerHandler{
|
||||||
|
{consumer: serviceConsumer, event: ServiceAnnounceEvent().ToString(), handler: func(ctx context.Context, env me.Envelope) error {
|
||||||
|
return svc.handleAnnounce(ctx, env)
|
||||||
|
}},
|
||||||
|
{consumer: gatewayConsumer, event: GatewayAnnounceEvent().ToString(), handler: func(ctx context.Context, env me.Envelope) error {
|
||||||
|
return svc.handleAnnounce(ctx, env)
|
||||||
|
}},
|
||||||
|
{consumer: heartbeatConsumer, event: HeartbeatEvent().ToString(), handler: svc.handleHeartbeat},
|
||||||
|
{consumer: lookupConsumer, event: LookupRequestEvent().ToString(), handler: svc.handleLookup},
|
||||||
|
}
|
||||||
|
svc.initKV(msgBroker)
|
||||||
return svc, nil
|
return svc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +93,16 @@ func (s *RegistryService) Start() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.startOnce.Do(func() {
|
s.startOnce.Do(func() {
|
||||||
|
s.logInfo("Discovery registry service starting", zap.Int("consumers", len(s.consumers)), zap.Bool("kv_enabled", s.kv != nil))
|
||||||
for _, ch := range s.consumers {
|
for _, ch := range s.consumers {
|
||||||
ch := ch
|
ch := ch
|
||||||
go func() {
|
go func() {
|
||||||
if err := ch.consumer.ConsumeMessages(ch.handler); err != nil && s.logger != nil {
|
if err := ch.consumer.ConsumeMessages(ch.handler); err != nil && s.logger != nil {
|
||||||
s.logger.Warn("Discovery consumer stopped with error", zap.Error(err))
|
s.logger.Warn("Discovery consumer stopped with error", zap.String("event", ch.event), zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
s.startKVWatch()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,18 +116,29 @@ func (s *RegistryService) Stop() {
|
|||||||
ch.consumer.Close()
|
ch.consumer.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.kvWatcher != nil {
|
||||||
|
_ = s.kvWatcher.Stop()
|
||||||
|
}
|
||||||
|
s.logInfo("Discovery registry service stopped")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RegistryService) handleAnnounce(_ context.Context, env me.Envelope) error {
|
func (s *RegistryService) handleAnnounce(_ context.Context, env me.Envelope) error {
|
||||||
var payload Announcement
|
var payload Announcement
|
||||||
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
|
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
|
||||||
s.logWarn("Failed to decode discovery announce payload", zap.Error(err))
|
fields := append(envelopeFields(env), zap.Int("data_len", len(env.GetData())), zap.Error(err))
|
||||||
|
s.logWarn("Failed to decode discovery announce payload", fields...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(payload.InstanceID) == "" {
|
||||||
|
fields := append(envelopeFields(env), announcementFields(payload)...)
|
||||||
|
s.logWarn("Discovery announce missing instance id", fields...)
|
||||||
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
result := s.registry.UpsertFromAnnouncement(payload, now)
|
result := s.registry.UpsertFromAnnouncement(payload, now)
|
||||||
|
s.persistEntry(result.Entry)
|
||||||
if result.IsNew || result.BecameHealthy {
|
if result.IsNew || result.BecameHealthy {
|
||||||
|
s.logInfo("Discovery registry entry updated", append(entryFields(result.Entry), zap.Bool("is_new", result.IsNew), zap.Bool("became_healthy", result.BecameHealthy))...)
|
||||||
s.publishRefresh(result.Entry)
|
s.publishRefresh(result.Entry)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -129,37 +147,48 @@ func (s *RegistryService) handleAnnounce(_ context.Context, env me.Envelope) err
|
|||||||
func (s *RegistryService) handleHeartbeat(_ context.Context, env me.Envelope) error {
|
func (s *RegistryService) handleHeartbeat(_ context.Context, env me.Envelope) error {
|
||||||
var payload Heartbeat
|
var payload Heartbeat
|
||||||
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
|
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
|
||||||
s.logWarn("Failed to decode discovery heartbeat payload", zap.Error(err))
|
fields := append(envelopeFields(env), zap.Int("data_len", len(env.GetData())), zap.Error(err))
|
||||||
|
s.logWarn("Failed to decode discovery heartbeat payload", fields...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if payload.ID == "" {
|
if strings.TrimSpace(payload.InstanceID) == "" && strings.TrimSpace(payload.ID) == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(payload.InstanceID) == "" {
|
||||||
|
fields := append(envelopeFields(env), zap.String("id", payload.ID))
|
||||||
|
s.logWarn("Discovery heartbeat missing instance id", fields...)
|
||||||
|
}
|
||||||
ts := time.Unix(payload.TS, 0)
|
ts := time.Unix(payload.TS, 0)
|
||||||
if ts.Unix() <= 0 {
|
if ts.Unix() <= 0 {
|
||||||
ts = time.Now()
|
ts = time.Now()
|
||||||
}
|
}
|
||||||
result, ok := s.registry.UpdateHeartbeat(payload.ID, strings.TrimSpace(payload.Status), ts, time.Now())
|
results := s.registry.UpdateHeartbeat(payload.ID, payload.InstanceID, strings.TrimSpace(payload.Status), ts, time.Now())
|
||||||
if ok && result.BecameHealthy {
|
for _, result := range results {
|
||||||
s.publishRefresh(result.Entry)
|
if result.BecameHealthy {
|
||||||
|
s.logInfo("Discovery registry entry became healthy", append(entryFields(result.Entry), zap.String("status", result.Entry.Status))...)
|
||||||
|
s.publishRefresh(result.Entry)
|
||||||
|
}
|
||||||
|
s.persistEntry(result.Entry)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RegistryService) handleLookup(_ context.Context, env me.Envelope) error {
|
func (s *RegistryService) handleLookup(_ context.Context, env me.Envelope) error {
|
||||||
if s.producer == nil {
|
if s.producer == nil {
|
||||||
s.logWarn("Discovery lookup request ignored: producer not configured")
|
s.logWarn("Discovery lookup request ignored: producer not configured", envelopeFields(env)...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var payload LookupRequest
|
var payload LookupRequest
|
||||||
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
|
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
|
||||||
s.logWarn("Failed to decode discovery lookup payload", zap.Error(err))
|
fields := append(envelopeFields(env), zap.Int("data_len", len(env.GetData())), zap.Error(err))
|
||||||
|
s.logWarn("Failed to decode discovery lookup payload", fields...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp := s.registry.Lookup(time.Now())
|
resp := s.registry.Lookup(time.Now())
|
||||||
resp.RequestID = strings.TrimSpace(payload.RequestID)
|
resp.RequestID = strings.TrimSpace(payload.RequestID)
|
||||||
if err := s.producer.SendMessage(NewLookupResponseEnvelope(s.sender, resp)); err != nil {
|
if err := s.producer.SendMessage(NewLookupResponseEnvelope(s.sender, resp)); err != nil {
|
||||||
s.logWarn("Failed to publish discovery lookup response", zap.Error(err))
|
fields := []zap.Field{zap.String("request_id", resp.RequestID), zap.Error(err)}
|
||||||
|
s.logWarn("Failed to publish discovery lookup response", fields...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -170,13 +199,99 @@ func (s *RegistryService) publishRefresh(entry RegistryEntry) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload := RefreshEvent{
|
payload := RefreshEvent{
|
||||||
Service: entry.Service,
|
InstanceID: entry.InstanceID,
|
||||||
Rail: entry.Rail,
|
Service: entry.Service,
|
||||||
Network: entry.Network,
|
Rail: entry.Rail,
|
||||||
Message: "new module available",
|
Network: entry.Network,
|
||||||
|
Message: "new module available",
|
||||||
}
|
}
|
||||||
if err := s.producer.SendMessage(NewRefreshUIEnvelope(s.sender, payload)); err != nil {
|
if err := s.producer.SendMessage(NewRefreshUIEnvelope(s.sender, payload)); err != nil {
|
||||||
s.logWarn("Failed to publish discovery refresh event", zap.Error(err))
|
fields := append(entryFields(entry), zap.Error(err))
|
||||||
|
s.logWarn("Failed to publish discovery refresh event", fields...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type jetStreamProvider interface {
|
||||||
|
JetStream() nats.JetStreamContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegistryService) initKV(msgBroker mb.Broker) {
|
||||||
|
if s == nil || msgBroker == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
provider, ok := msgBroker.(jetStreamProvider)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
js := provider.JetStream()
|
||||||
|
if js == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store, err := NewKVStore(s.logger, js, "")
|
||||||
|
if err != nil {
|
||||||
|
s.logWarn("Failed to initialise discovery KV store", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.kv = store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegistryService) startKVWatch() {
|
||||||
|
if s == nil || s.kv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
watcher, err := s.kv.WatchAll()
|
||||||
|
if err != nil {
|
||||||
|
s.logWarn("Failed to start discovery KV watch", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.kvWatcher = watcher
|
||||||
|
if bucket := s.kv.Bucket(); bucket != "" {
|
||||||
|
s.logInfo("Discovery KV watch started", zap.String("bucket", bucket))
|
||||||
|
}
|
||||||
|
go s.consumeKVUpdates(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegistryService) consumeKVUpdates(watcher nats.KeyWatcher) {
|
||||||
|
if s == nil || watcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for entry := range watcher.Updates() {
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch entry.Operation() {
|
||||||
|
case nats.KeyValueDelete, nats.KeyValuePurge:
|
||||||
|
key := registryKeyFromKVKey(entry.Key())
|
||||||
|
if key != "" {
|
||||||
|
if s.registry.Delete(key) {
|
||||||
|
s.logInfo("Discovery registry entry removed", zap.String("key", key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case nats.KeyValuePut:
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload RegistryEntry
|
||||||
|
if err := json.Unmarshal(entry.Value(), &payload); err != nil {
|
||||||
|
s.logWarn("Failed to decode discovery KV entry", zap.String("key", entry.Key()), zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result := s.registry.UpsertEntry(payload, time.Now())
|
||||||
|
if result.IsNew || result.BecameHealthy {
|
||||||
|
s.logInfo("Discovery registry entry updated from KV", append(entryFields(result.Entry), zap.Bool("is_new", result.IsNew), zap.Bool("became_healthy", result.BecameHealthy))...)
|
||||||
|
s.publishRefresh(result.Entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegistryService) persistEntry(entry RegistryEntry) {
|
||||||
|
if s == nil || s.kv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.kv.Put(entry); err != nil {
|
||||||
|
s.logWarn("Failed to persist discovery entry", append(entryFields(entry), zap.Error(err))...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,3 +301,10 @@ func (s *RegistryService) logWarn(message string, fields ...zap.Field) {
|
|||||||
}
|
}
|
||||||
s.logger.Warn(message, fields...)
|
s.logger.Warn(message, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RegistryService) logInfo(message string, fields ...zap.Field) {
|
||||||
|
if s.logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.Info(message, fields...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Limits struct {
|
|||||||
|
|
||||||
type Announcement struct {
|
type Announcement struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
InstanceID string `bson:"instanceId" json:"instanceId"`
|
||||||
Service string `json:"service"`
|
Service string `json:"service"`
|
||||||
Rail string `json:"rail,omitempty"`
|
Rail string `json:"rail,omitempty"`
|
||||||
Network string `json:"network,omitempty"`
|
Network string `json:"network,omitempty"`
|
||||||
@@ -27,14 +28,16 @@ type Announcement struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Heartbeat struct {
|
type Heartbeat struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
InstanceID string `json:"instanceId"`
|
||||||
TS int64 `json:"ts"`
|
Status string `json:"status"`
|
||||||
|
TS int64 `json:"ts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RefreshEvent struct {
|
type RefreshEvent struct {
|
||||||
Service string `json:"service,omitempty"`
|
InstanceID string `json:"instanceId,omitempty"`
|
||||||
Rail string `json:"rail,omitempty"`
|
Service string `json:"service,omitempty"`
|
||||||
Network string `json:"network,omitempty"`
|
Rail string `json:"rail,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Network string `json:"network,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
126
api/pkg/discovery/watcher.go
Normal file
126
api/pkg/discovery/watcher.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
mb "github.com/tech/sendico/pkg/messaging/broker"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegistryWatcher struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
registry *Registry
|
||||||
|
kv *KVStore
|
||||||
|
watcher nats.KeyWatcher
|
||||||
|
|
||||||
|
stopOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Registry) (*RegistryWatcher, error) {
|
||||||
|
if msgBroker == nil {
|
||||||
|
return nil, errors.New("discovery watcher: broker is nil")
|
||||||
|
}
|
||||||
|
if registry == nil {
|
||||||
|
registry = NewRegistry()
|
||||||
|
}
|
||||||
|
if logger != nil {
|
||||||
|
logger = logger.Named("discovery_watcher")
|
||||||
|
}
|
||||||
|
provider, ok := msgBroker.(jetStreamProvider)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("discovery watcher: jetstream not available")
|
||||||
|
}
|
||||||
|
js := provider.JetStream()
|
||||||
|
if js == nil {
|
||||||
|
return nil, errors.New("discovery watcher: jetstream not configured")
|
||||||
|
}
|
||||||
|
store, err := NewKVStore(logger, js, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RegistryWatcher{
|
||||||
|
logger: logger,
|
||||||
|
registry: registry,
|
||||||
|
kv: store,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RegistryWatcher) Start() error {
|
||||||
|
if w == nil || w.kv == nil {
|
||||||
|
return errors.New("discovery watcher: not configured")
|
||||||
|
}
|
||||||
|
watcher, err := w.kv.WatchAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.watcher = watcher
|
||||||
|
if w.logger != nil {
|
||||||
|
w.logger.Info("Discovery registry watcher started", zap.String("bucket", w.kv.Bucket()))
|
||||||
|
}
|
||||||
|
go w.consume(watcher)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RegistryWatcher) Stop() {
|
||||||
|
if w == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.stopOnce.Do(func() {
|
||||||
|
if w.watcher != nil {
|
||||||
|
_ = w.watcher.Stop()
|
||||||
|
}
|
||||||
|
if w.logger != nil {
|
||||||
|
w.logger.Info("Discovery registry watcher stopped")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RegistryWatcher) Registry() *Registry {
|
||||||
|
if w == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return w.registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RegistryWatcher) consume(watcher nats.KeyWatcher) {
|
||||||
|
if w == nil || watcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for entry := range watcher.Updates() {
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch entry.Operation() {
|
||||||
|
case nats.KeyValueDelete, nats.KeyValuePurge:
|
||||||
|
key := registryKeyFromKVKey(entry.Key())
|
||||||
|
if key != "" {
|
||||||
|
if w.registry.Delete(key) && w.logger != nil {
|
||||||
|
w.logger.Info("Discovery registry entry removed", zap.String("key", key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case nats.KeyValuePut:
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload RegistryEntry
|
||||||
|
if err := json.Unmarshal(entry.Value(), &payload); err != nil {
|
||||||
|
if w.logger != nil {
|
||||||
|
w.logger.Warn("Failed to decode discovery KV entry", zap.String("key", entry.Key()), zap.Error(err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result := w.registry.UpsertEntry(payload, time.Now())
|
||||||
|
if w.logger != nil && (result.IsNew || result.BecameHealthy) {
|
||||||
|
fields := append(entryFields(result.Entry), zap.Bool("is_new", result.IsNew), zap.Bool("became_healthy", result.BecameHealthy))
|
||||||
|
w.logger.Info("Discovery registry entry updated from KV", fields...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ type natsSubscriotions = map[string]*TopicSubscription
|
|||||||
|
|
||||||
type NatsBroker struct {
|
type NatsBroker struct {
|
||||||
nc *nats.Conn
|
nc *nats.Conn
|
||||||
|
js nats.JetStreamContext
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
topicSubs natsSubscriotions
|
topicSubs natsSubscriotions
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -78,23 +80,46 @@ func loadEnv(settings *nc.Settings, l *zap.Logger) (*envConfig, error) {
|
|||||||
|
|
||||||
func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, error) {
|
func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, error) {
|
||||||
l := logger.Named("broker")
|
l := logger.Named("broker")
|
||||||
// Helper function to get environment variables
|
var err error
|
||||||
cfg, err := loadEnv(settings, l)
|
var cfg *envConfig
|
||||||
if err != nil {
|
var natsURL string
|
||||||
return nil, err
|
if settings != nil && strings.TrimSpace(settings.URLEnv) != "" {
|
||||||
|
urlVal := strings.TrimSpace(os.Getenv(settings.URLEnv))
|
||||||
|
if urlVal != "" {
|
||||||
|
natsURL = urlVal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if natsURL == "" {
|
||||||
|
// Helper function to get environment variables
|
||||||
|
cfg, err = loadEnv(settings, l)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "nats",
|
Scheme: "nats",
|
||||||
Host: net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)),
|
Host: net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)),
|
||||||
|
}
|
||||||
|
natsURL = u.String()
|
||||||
}
|
}
|
||||||
natsURL := u.String()
|
|
||||||
|
|
||||||
opts := []nats.Option{
|
opts := []nats.Option{
|
||||||
nats.Name(settings.NATSName),
|
nats.Name(settings.NATSName),
|
||||||
nats.MaxReconnects(settings.MaxReconnects),
|
nats.MaxReconnects(settings.MaxReconnects),
|
||||||
nats.ReconnectWait(time.Duration(settings.ReconnectWait) * time.Second),
|
nats.ReconnectWait(time.Duration(settings.ReconnectWait) * time.Second),
|
||||||
nats.UserInfo(cfg.User, cfg.Password),
|
}
|
||||||
|
if cfg != nil {
|
||||||
|
opts = append(opts, nats.UserInfo(cfg.User, cfg.Password))
|
||||||
|
} else if settings != nil {
|
||||||
|
userEnv := strings.TrimSpace(settings.UsernameEnv)
|
||||||
|
passEnv := strings.TrimSpace(settings.PasswordEnv)
|
||||||
|
if userEnv != "" && passEnv != "" {
|
||||||
|
user := strings.TrimSpace(os.Getenv(userEnv))
|
||||||
|
pass := strings.TrimSpace(os.Getenv(passEnv))
|
||||||
|
if user != "" || pass != "" {
|
||||||
|
opts = append(opts, nats.UserInfo(user, pass))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &NatsBroker{
|
res := &NatsBroker{
|
||||||
@@ -106,8 +131,18 @@ func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, e
|
|||||||
l.Error("Failed to connect to NATS", zap.String("url", natsURL), zap.Error(err))
|
l.Error("Failed to connect to NATS", zap.String("url", natsURL), zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if res.js, err = res.nc.JetStream(); err != nil {
|
||||||
|
l.Warn("Failed to initialise JetStream context", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Connected to NATS", zap.String("broker", settings.NATSName),
|
logger.Info("Connected to NATS", zap.String("broker", settings.NATSName),
|
||||||
zap.String("url", fmt.Sprintf("nats://%s@%s", cfg.User, net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)))))
|
zap.String("url", natsURL))
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *NatsBroker) JetStream() nats.JetStreamContext {
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b.js
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
lf "github.com/tech/sendico/pkg/mlogger/factory"
|
lf "github.com/tech/sendico/pkg/mlogger/factory"
|
||||||
"github.com/tech/sendico/pkg/server"
|
"github.com/tech/sendico/pkg/server"
|
||||||
@@ -28,6 +29,7 @@ func prepareLogger() mlogger.Logger {
|
|||||||
|
|
||||||
func RunServer(rootLoggerName string, av version.Printer, factory server.ServerFactoryT) {
|
func RunServer(rootLoggerName string, av version.Printer, factory server.ServerFactoryT) {
|
||||||
logger := prepareLogger().Named(rootLoggerName)
|
logger := prepareLogger().Named(rootLoggerName)
|
||||||
|
logger = logger.With(zap.String("instance_id", discovery.InstanceID()))
|
||||||
defer logger.Sync()
|
defer logger.Sync()
|
||||||
|
|
||||||
// Show version information
|
// Show version information
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ api:
|
|||||||
message_broker:
|
message_broker:
|
||||||
driver: NATS
|
driver: NATS
|
||||||
settings:
|
settings:
|
||||||
|
url_env: NATS_URL
|
||||||
host_env: NATS_HOST
|
host_env: NATS_HOST
|
||||||
port_env: NATS_PORT
|
port_env: NATS_PORT
|
||||||
username_env: NATS_USER
|
username_env: NATS_USER
|
||||||
@@ -54,7 +55,7 @@ api:
|
|||||||
length: 32
|
length: 32
|
||||||
password:
|
password:
|
||||||
token_length: 32
|
token_length: 32
|
||||||
checks:
|
check:
|
||||||
min_length: 8
|
min_length: 8
|
||||||
digit: true
|
digit: true
|
||||||
upper: true
|
upper: true
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ NATS_MONITORING_PORT=8222
|
|||||||
NATS_PROMETHEUS_PORT=7777
|
NATS_PROMETHEUS_PORT=7777
|
||||||
NATS_COMPOSE_PROJECT=sendico-nats
|
NATS_COMPOSE_PROJECT=sendico-nats
|
||||||
|
|
||||||
|
# Discovery service
|
||||||
|
DISCOVERY_DIR=discovery
|
||||||
|
DISCOVERY_COMPOSE_PROJECT=sendico-discovery
|
||||||
|
DISCOVERY_SERVICE_NAME=sendico_discovery
|
||||||
|
DISCOVERY_METRICS_PORT=9405
|
||||||
|
|
||||||
|
|
||||||
# Shared Mongo settings for FX services
|
# Shared Mongo settings for FX services
|
||||||
FX_MONGO_HOST=sendico_db1
|
FX_MONGO_HOST=sendico_db1
|
||||||
|
|||||||
40
ci/prod/compose/discovery.dockerfile
Normal file
40
ci/prod/compose/discovery.dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
ARG TARGETOS=linux
|
||||||
|
ARG TARGETARCH=amd64
|
||||||
|
|
||||||
|
FROM golang:alpine AS build
|
||||||
|
ARG APP_VERSION=dev
|
||||||
|
ARG GIT_REV=unknown
|
||||||
|
ARG BUILD_BRANCH=unknown
|
||||||
|
ARG BUILD_DATE=unknown
|
||||||
|
ARG BUILD_USER=ci
|
||||||
|
ENV GO111MODULE=on
|
||||||
|
ENV PATH="/go/bin:${PATH}"
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN apk add --no-cache bash git build-base protoc protobuf-dev \
|
||||||
|
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
||||||
|
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest \
|
||||||
|
&& bash ci/scripts/proto/generate.sh
|
||||||
|
WORKDIR /src/api/discovery
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
|
||||||
|
go build -trimpath -ldflags "\
|
||||||
|
-s -w \
|
||||||
|
-X github.com/tech/sendico/discovery/internal/appversion.Version=${APP_VERSION} \
|
||||||
|
-X github.com/tech/sendico/discovery/internal/appversion.Revision=${GIT_REV} \
|
||||||
|
-X github.com/tech/sendico/discovery/internal/appversion.Branch=${BUILD_BRANCH} \
|
||||||
|
-X github.com/tech/sendico/discovery/internal/appversion.BuildUser=${BUILD_USER} \
|
||||||
|
-X github.com/tech/sendico/discovery/internal/appversion.BuildDate=${BUILD_DATE}" \
|
||||||
|
-o /out/discovery .
|
||||||
|
|
||||||
|
FROM alpine:latest AS runtime
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata wget
|
||||||
|
WORKDIR /app
|
||||||
|
COPY api/discovery/config.yml /app/config.yml
|
||||||
|
COPY --from=build /out/discovery /app/discovery
|
||||||
|
EXPOSE 9405
|
||||||
|
ENTRYPOINT ["/app/discovery"]
|
||||||
|
CMD ["--config.file", "/app/config.yml"]
|
||||||
37
ci/prod/compose/discovery.yml
Normal file
37
ci/prod/compose/discovery.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Compose v2 - Discovery service
|
||||||
|
|
||||||
|
x-common-env: &common-env
|
||||||
|
env_file:
|
||||||
|
- ../env/.env.runtime
|
||||||
|
- ../env/.env.version
|
||||||
|
|
||||||
|
networks:
|
||||||
|
sendico-net:
|
||||||
|
external: true
|
||||||
|
name: sendico-net
|
||||||
|
|
||||||
|
services:
|
||||||
|
sendico_discovery:
|
||||||
|
<<: *common-env
|
||||||
|
container_name: sendico-discovery
|
||||||
|
restart: unless-stopped
|
||||||
|
image: ${REGISTRY_URL}/discovery/service:${APP_V}
|
||||||
|
pull_policy: always
|
||||||
|
environment:
|
||||||
|
NATS_URL: ${NATS_URL}
|
||||||
|
NATS_HOST: ${NATS_HOST}
|
||||||
|
NATS_PORT: ${NATS_PORT}
|
||||||
|
NATS_USER: ${NATS_USER}
|
||||||
|
NATS_PASSWORD: ${NATS_PASSWORD}
|
||||||
|
DISCOVERY_METRICS_PORT: ${DISCOVERY_METRICS_PORT}
|
||||||
|
command: ["--config.file", "/app/config.yml"]
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:${DISCOVERY_METRICS_PORT}:${DISCOVERY_METRICS_PORT}"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL","wget -qO- http://localhost:${DISCOVERY_METRICS_PORT}/health | grep -q '\"status\":\"ok\"'"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- sendico-net
|
||||||
139
ci/prod/scripts/deploy/discovery.sh
Normal file
139
ci/prod/scripts/deploy/discovery.sh
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
[[ "${DEBUG_DEPLOY:-0}" = "1" ]] && set -x
|
||||||
|
trap 'echo "[deploy-discovery] error at line $LINENO" >&2' ERR
|
||||||
|
|
||||||
|
: "${REMOTE_BASE:?missing REMOTE_BASE}"
|
||||||
|
: "${SSH_USER:?missing SSH_USER}"
|
||||||
|
: "${SSH_HOST:?missing SSH_HOST}"
|
||||||
|
: "${DISCOVERY_DIR:?missing DISCOVERY_DIR}"
|
||||||
|
: "${DISCOVERY_COMPOSE_PROJECT:?missing DISCOVERY_COMPOSE_PROJECT}"
|
||||||
|
: "${DISCOVERY_SERVICE_NAME:?missing DISCOVERY_SERVICE_NAME}"
|
||||||
|
|
||||||
|
REMOTE_DIR="${REMOTE_BASE%/}/${DISCOVERY_DIR}"
|
||||||
|
REMOTE_TARGET="${SSH_USER}@${SSH_HOST}"
|
||||||
|
COMPOSE_FILE="discovery.yml"
|
||||||
|
SERVICE_NAMES="${DISCOVERY_SERVICE_NAME}"
|
||||||
|
|
||||||
|
REQUIRED_SECRETS=(
|
||||||
|
NATS_USER
|
||||||
|
NATS_PASSWORD
|
||||||
|
NATS_URL
|
||||||
|
)
|
||||||
|
|
||||||
|
for var in "${REQUIRED_SECRETS[@]}"; do
|
||||||
|
if [[ -z "${!var:-}" ]]; then
|
||||||
|
echo "missing required secret env: ${var}" >&2
|
||||||
|
exit 65
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -s .env.version ]]; then
|
||||||
|
echo ".env.version is missing; run version step first" >&2
|
||||||
|
exit 66
|
||||||
|
fi
|
||||||
|
|
||||||
|
b64enc() {
|
||||||
|
printf '%s' "$1" | base64 | tr -d '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
NATS_USER_B64="$(b64enc "${NATS_USER}")"
|
||||||
|
NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")"
|
||||||
|
NATS_URL_B64="$(b64enc "${NATS_URL}")"
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-i /root/.ssh/id_rsa
|
||||||
|
-o StrictHostKeyChecking=no
|
||||||
|
-o UserKnownHostsFile=/dev/null
|
||||||
|
-o LogLevel=ERROR
|
||||||
|
-q
|
||||||
|
)
|
||||||
|
if [[ "${DEBUG_DEPLOY:-0}" = "1" ]]; then
|
||||||
|
SSH_OPTS=("${SSH_OPTS[@]/-q/}" -vv)
|
||||||
|
fi
|
||||||
|
|
||||||
|
RSYNC_FLAGS=(-az --delete)
|
||||||
|
[[ "${DEBUG_DEPLOY:-0}" = "1" ]] && RSYNC_FLAGS=(-avz --delete)
|
||||||
|
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" "mkdir -p ${REMOTE_DIR}/{compose,env}"
|
||||||
|
|
||||||
|
rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/compose/ "$REMOTE_TARGET:${REMOTE_DIR}/compose/"
|
||||||
|
rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/.env.runtime "$REMOTE_TARGET:${REMOTE_DIR}/env/.env.runtime"
|
||||||
|
rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" .env.version "$REMOTE_TARGET:${REMOTE_DIR}/env/.env.version"
|
||||||
|
|
||||||
|
SERVICES_LINE="${SERVICE_NAMES}"
|
||||||
|
|
||||||
|
ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \
|
||||||
|
REMOTE_DIR="$REMOTE_DIR" \
|
||||||
|
COMPOSE_FILE="$COMPOSE_FILE" \
|
||||||
|
COMPOSE_PROJECT="$DISCOVERY_COMPOSE_PROJECT" \
|
||||||
|
SERVICES_LINE="$SERVICES_LINE" \
|
||||||
|
NATS_USER_B64="$NATS_USER_B64" \
|
||||||
|
NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \
|
||||||
|
NATS_URL_B64="$NATS_URL_B64" \
|
||||||
|
bash -s <<'EOSSH'
|
||||||
|
set -euo pipefail
|
||||||
|
cd "${REMOTE_DIR}/compose"
|
||||||
|
set -a
|
||||||
|
. ../env/.env.runtime
|
||||||
|
load_kv_file() {
|
||||||
|
local file="$1"
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in
|
||||||
|
''|\#*) continue ;;
|
||||||
|
esac
|
||||||
|
if printf '%s' "$line" | grep -Eq '^[[:alpha:]_][[:alnum:]_]*='; then
|
||||||
|
local key="${line%%=*}"
|
||||||
|
local value="${line#*=}"
|
||||||
|
key="$(printf '%s' "$key" | tr -d '[:space:]')"
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
if [[ -n "$key" ]]; then
|
||||||
|
export "$key=$value"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <"$file"
|
||||||
|
}
|
||||||
|
load_kv_file ../env/.env.version
|
||||||
|
set +a
|
||||||
|
|
||||||
|
if base64 -d >/dev/null 2>&1 <<<'AA=='; then
|
||||||
|
BASE64_DECODE_FLAG='-d'
|
||||||
|
else
|
||||||
|
BASE64_DECODE_FLAG='--decode'
|
||||||
|
fi
|
||||||
|
|
||||||
|
decode_b64() {
|
||||||
|
val="$1"
|
||||||
|
if [[ -z "$val" ]]; then
|
||||||
|
printf ''
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
printf '%s' "$val" | base64 "${BASE64_DECODE_FLAG}"
|
||||||
|
}
|
||||||
|
|
||||||
|
NATS_USER="$(decode_b64 "$NATS_USER_B64")"
|
||||||
|
NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")"
|
||||||
|
NATS_URL="$(decode_b64 "$NATS_URL_B64")"
|
||||||
|
|
||||||
|
export NATS_USER NATS_PASSWORD NATS_URL
|
||||||
|
COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT"
|
||||||
|
export COMPOSE_PROJECT_NAME
|
||||||
|
read -r -a SERVICES <<<"${SERVICES_LINE}"
|
||||||
|
|
||||||
|
pull_cmd=(docker compose -f "$COMPOSE_FILE" pull)
|
||||||
|
up_cmd=(docker compose -f "$COMPOSE_FILE" up -d --remove-orphans)
|
||||||
|
ps_cmd=(docker compose -f "$COMPOSE_FILE" ps)
|
||||||
|
if [[ "${#SERVICES[@]}" -gt 0 ]]; then
|
||||||
|
pull_cmd+=("${SERVICES[@]}")
|
||||||
|
up_cmd+=("${SERVICES[@]}")
|
||||||
|
ps_cmd+=("${SERVICES[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${pull_cmd[@]}"
|
||||||
|
"${up_cmd[@]}"
|
||||||
|
"${ps_cmd[@]}"
|
||||||
|
|
||||||
|
date -Is > .last_deploy
|
||||||
|
logger -t "deploy-${COMPOSE_PROJECT_NAME}" "${COMPOSE_PROJECT_NAME} deployed at $(date -Is) in ${REMOTE_DIR}"
|
||||||
|
EOSSH
|
||||||
85
ci/scripts/discovery/build-image.sh
Normal file
85
ci/scripts/discovery/build-image.sh
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if ! set -o pipefail 2>/dev/null; then
|
||||||
|
:
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||||
|
cd "${REPO_ROOT}"
|
||||||
|
|
||||||
|
sh ci/scripts/common/ensure_env_version.sh
|
||||||
|
|
||||||
|
normalize_env_file() {
|
||||||
|
file="$1"
|
||||||
|
tmp="${file}.tmp.$$"
|
||||||
|
tr -d '\r' <"$file" >"$tmp"
|
||||||
|
mv "$tmp" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env_file() {
|
||||||
|
file="$1"
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in
|
||||||
|
''|\#*) continue ;;
|
||||||
|
esac
|
||||||
|
key="${line%%=*}"
|
||||||
|
value="${line#*=}"
|
||||||
|
key="$(printf '%s' "$key" | tr -d '[:space:]')"
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
export "$key=$value"
|
||||||
|
done <"$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
DISCOVERY_ENV_NAME="${DISCOVERY_ENV:-prod}"
|
||||||
|
RUNTIME_ENV_FILE="./ci/${DISCOVERY_ENV_NAME}/.env.runtime"
|
||||||
|
|
||||||
|
if [ ! -f "${RUNTIME_ENV_FILE}" ]; then
|
||||||
|
echo "[discovery-build] runtime env file not found: ${RUNTIME_ENV_FILE}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
normalize_env_file "${RUNTIME_ENV_FILE}"
|
||||||
|
normalize_env_file ./.env.version
|
||||||
|
|
||||||
|
load_env_file "${RUNTIME_ENV_FILE}"
|
||||||
|
load_env_file ./.env.version
|
||||||
|
|
||||||
|
REGISTRY_URL="${REGISTRY_URL:?missing REGISTRY_URL}"
|
||||||
|
APP_V="${APP_V:?missing APP_V}"
|
||||||
|
DISCOVERY_DOCKERFILE="${DISCOVERY_DOCKERFILE:?missing DISCOVERY_DOCKERFILE}"
|
||||||
|
DISCOVERY_IMAGE_PATH="${DISCOVERY_IMAGE_PATH:?missing DISCOVERY_IMAGE_PATH}"
|
||||||
|
|
||||||
|
REGISTRY_HOST="${REGISTRY_URL#http://}"
|
||||||
|
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
||||||
|
REGISTRY_USER="$(cat secrets/REGISTRY_USER)"
|
||||||
|
REGISTRY_PASSWORD="$(cat secrets/REGISTRY_PASSWORD)"
|
||||||
|
: "${REGISTRY_USER:?missing registry user}"
|
||||||
|
: "${REGISTRY_PASSWORD:?missing registry password}"
|
||||||
|
|
||||||
|
mkdir -p /kaniko/.docker
|
||||||
|
AUTH_B64="$(printf '%s:%s' "$REGISTRY_USER" "$REGISTRY_PASSWORD" | base64 | tr -d '\n')"
|
||||||
|
cat <<JSON >/kaniko/.docker/config.json
|
||||||
|
{
|
||||||
|
"auths": {
|
||||||
|
"https://${REGISTRY_HOST}": { "auth": "${AUTH_B64}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
BUILD_CONTEXT="${DISCOVERY_BUILD_CONTEXT:-${WOODPECKER_WORKSPACE:-${CI_WORKSPACE:-${PWD:-/workspace}}}}"
|
||||||
|
if [ ! -d "${BUILD_CONTEXT}" ]; then
|
||||||
|
BUILD_CONTEXT="/workspace"
|
||||||
|
fi
|
||||||
|
|
||||||
|
/kaniko/executor \
|
||||||
|
--context "${BUILD_CONTEXT}" \
|
||||||
|
--dockerfile "${DISCOVERY_DOCKERFILE}" \
|
||||||
|
--destination "${REGISTRY_URL}/${DISCOVERY_IMAGE_PATH}:${APP_V}" \
|
||||||
|
--build-arg APP_VERSION="${APP_V}" \
|
||||||
|
--build-arg GIT_REV="${GIT_REV}" \
|
||||||
|
--build-arg BUILD_BRANCH="${BUILD_BRANCH}" \
|
||||||
|
--build-arg BUILD_DATE="${BUILD_DATE}" \
|
||||||
|
--build-arg BUILD_USER="${BUILD_USER}" \
|
||||||
|
--single-snapshot
|
||||||
57
ci/scripts/discovery/deploy.sh
Normal file
57
ci/scripts/discovery/deploy.sh
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if ! set -o pipefail 2>/dev/null; then
|
||||||
|
:
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||||
|
cd "${REPO_ROOT}"
|
||||||
|
|
||||||
|
sh ci/scripts/common/ensure_env_version.sh
|
||||||
|
|
||||||
|
normalize_env_file() {
|
||||||
|
file="$1"
|
||||||
|
tmp="${file}.tmp.$$"
|
||||||
|
tr -d '\r' <"$file" >"$tmp"
|
||||||
|
mv "$tmp" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env_file() {
|
||||||
|
file="$1"
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in
|
||||||
|
''|\#*) continue ;;
|
||||||
|
esac
|
||||||
|
key="${line%%=*}"
|
||||||
|
value="${line#*=}"
|
||||||
|
key="$(printf '%s' "$key" | tr -d '[:space:]')"
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
export "$key=$value"
|
||||||
|
done <"$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
DISCOVERY_ENV_NAME="${DISCOVERY_ENV:-prod}"
|
||||||
|
RUNTIME_ENV_FILE="./ci/${DISCOVERY_ENV_NAME}/.env.runtime"
|
||||||
|
|
||||||
|
if [ ! -f "${RUNTIME_ENV_FILE}" ]; then
|
||||||
|
echo "[discovery-deploy] runtime env file not found: ${RUNTIME_ENV_FILE}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
normalize_env_file "${RUNTIME_ENV_FILE}"
|
||||||
|
normalize_env_file ./.env.version
|
||||||
|
|
||||||
|
load_env_file "${RUNTIME_ENV_FILE}"
|
||||||
|
load_env_file ./.env.version
|
||||||
|
|
||||||
|
: "${NATS_HOST:?missing NATS_HOST}"
|
||||||
|
: "${NATS_PORT:?missing NATS_PORT}"
|
||||||
|
|
||||||
|
export NATS_USER="$(./ci/vlt kv_get kv sendico/nats user)"
|
||||||
|
export NATS_PASSWORD="$(./ci/vlt kv_get kv sendico/nats password)"
|
||||||
|
export NATS_URL="nats://${NATS_USER}:${NATS_PASSWORD}@${NATS_HOST}:${NATS_PORT}"
|
||||||
|
|
||||||
|
bash ci/prod/scripts/bootstrap/network.sh
|
||||||
|
bash ci/prod/scripts/deploy/discovery.sh
|
||||||
Reference in New Issue
Block a user