cb-582 #583
90
.woodpecker/callbacks.yml
Normal file
90
.woodpecker/callbacks.yml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- CALLBACKS_IMAGE_PATH: edge/callbacks
|
||||||
|
CALLBACKS_DOCKERFILE: ci/prod/compose/callbacks.dockerfile
|
||||||
|
CALLBACKS_MONGO_SECRET_PATH: sendico/db
|
||||||
|
CALLBACKS_VAULT_SECRET_PATH: sendico/edge/callbacks/vault
|
||||||
|
CALLBACKS_ENV: prod
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
path:
|
||||||
|
include:
|
||||||
|
- api/edge/callbacks/**
|
||||||
|
- api/proto/**
|
||||||
|
- api/pkg/**
|
||||||
|
- ci/prod/**
|
||||||
|
- .woodpecker/callbacks.yml
|
||||||
|
ignore_message: '[rebuild]'
|
||||||
|
|
||||||
|
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: backend-tests
|
||||||
|
image: golang:alpine
|
||||||
|
depends_on: [ proto ]
|
||||||
|
commands:
|
||||||
|
- set -eu
|
||||||
|
- apk add --no-cache bash git build-base
|
||||||
|
- sh ci/scripts/common/run_backend_tests.sh callbacks
|
||||||
|
|
||||||
|
- 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: [ backend-tests, secrets ]
|
||||||
|
commands:
|
||||||
|
- sh ci/scripts/callbacks/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/callbacks/deploy.sh
|
||||||
6
Makefile
6
Makefile
@@ -38,7 +38,7 @@ help:
|
|||||||
@echo " make build-fx Build FX services (oracle, ingestor)"
|
@echo " make build-fx Build FX services (oracle, ingestor)"
|
||||||
@echo " make build-payments Build payment orchestrator"
|
@echo " make build-payments Build payment orchestrator"
|
||||||
@echo " make build-gateways Build gateway services (chain, tron, mntx, tgsettle)"
|
@echo " make build-gateways Build gateway services (chain, tron, mntx, tgsettle)"
|
||||||
@echo " make build-api Build API services (notification, bff)"
|
@echo " make build-api Build API services (notification, callbacks, bff)"
|
||||||
@echo " make build-frontend Build Flutter web frontend"
|
@echo " make build-frontend Build Flutter web frontend"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "$(YELLOW)Development:$(NC)"
|
@echo "$(YELLOW)Development:$(NC)"
|
||||||
@@ -225,6 +225,7 @@ services-up:
|
|||||||
dev-mntx-gateway \
|
dev-mntx-gateway \
|
||||||
dev-tgsettle-gateway \
|
dev-tgsettle-gateway \
|
||||||
dev-notification \
|
dev-notification \
|
||||||
|
dev-callbacks \
|
||||||
dev-bff \
|
dev-bff \
|
||||||
dev-frontend
|
dev-frontend
|
||||||
|
|
||||||
@@ -254,6 +255,7 @@ list-services:
|
|||||||
@echo " - dev-mntx-gateway :50075, :9405, :8084 (Card Payouts)"
|
@echo " - dev-mntx-gateway :50075, :9405, :8084 (Card Payouts)"
|
||||||
@echo " - dev-tgsettle-gateway :50080, :9406 (Telegram Settlements)"
|
@echo " - dev-tgsettle-gateway :50080, :9406 (Telegram Settlements)"
|
||||||
@echo " - dev-notification :8081 (Notifications)"
|
@echo " - dev-notification :8081 (Notifications)"
|
||||||
|
@echo " - dev-callbacks :9420 (Webhook Callbacks)"
|
||||||
@echo " - dev-bff :8080 (Backend for Frontend)"
|
@echo " - dev-bff :8080 (Backend for Frontend)"
|
||||||
@echo " - dev-frontend :3000 (Flutter Web UI)"
|
@echo " - dev-frontend :3000 (Flutter Web UI)"
|
||||||
|
|
||||||
@@ -285,7 +287,7 @@ build-gateways:
|
|||||||
|
|
||||||
build-api:
|
build-api:
|
||||||
@echo "$(GREEN)Building API services...$(NC)"
|
@echo "$(GREEN)Building API services...$(NC)"
|
||||||
@$(COMPOSE) build dev-notification dev-bff
|
@$(COMPOSE) build dev-notification dev-callbacks dev-bff
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
@echo "$(GREEN)Building frontend...$(NC)"
|
@echo "$(GREEN)Building frontend...$(NC)"
|
||||||
|
|||||||
64
README.md
64
README.md
@@ -28,6 +28,7 @@ Financial services platform providing payment orchestration, ledger accounting,
|
|||||||
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX |
|
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX |
|
||||||
| Notification | `api/notification/` | Notifications |
|
| Notification | `api/notification/` | Notifications |
|
||||||
| BFF | `api/edge/bff/` | Backend for frontend |
|
| BFF | `api/edge/bff/` | Backend for frontend |
|
||||||
|
| Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery |
|
||||||
| Frontend | `frontend/pweb/` | Flutter web UI |
|
| Frontend | `frontend/pweb/` | Flutter web UI |
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
@@ -70,7 +71,7 @@ make build-core # discovery, ledger, fees, documents
|
|||||||
make build-fx # oracle, ingestor
|
make build-fx # oracle, ingestor
|
||||||
make build-payments # orchestrator
|
make build-payments # orchestrator
|
||||||
make build-gateways # chain, tron, mntx, tgsettle
|
make build-gateways # chain, tron, mntx, tgsettle
|
||||||
make build-api # notification, bff
|
make build-api # notification, callbacks, bff
|
||||||
make build-frontend # Flutter web UI
|
make build-frontend # Flutter web UI
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -98,3 +99,64 @@ make update # Update all Go and Flutter dependencies
|
|||||||
make update-api # Update Go dependencies only
|
make update-api # Update Go dependencies only
|
||||||
make update-frontend # Update Flutter dependencies only
|
make update-frontend # Update Flutter dependencies only
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Callbacks Secret References
|
||||||
|
|
||||||
|
Callbacks (`api/edge/callbacks`) supports three secret reference formats:
|
||||||
|
|
||||||
|
- `env:MY_SECRET_ENV` to read from environment variables.
|
||||||
|
- `vault:some/path#field` to read a field from Vault KV v2.
|
||||||
|
- `some/path#field` to read from Vault KV v2 when `secrets.vault` is configured.
|
||||||
|
|
||||||
|
If `#field` is omitted, callbacks uses `secrets.vault.default_field` (default: `value`).
|
||||||
|
|
||||||
|
### Callbacks Vault Auth (Dev + Prod)
|
||||||
|
|
||||||
|
Callbacks now authenticates to Vault through a sidecar Vault Agent (AppRole), same pattern as chain/tron gateways.
|
||||||
|
|
||||||
|
- Dev compose:
|
||||||
|
- service: `dev-callbacks-vault-agent`
|
||||||
|
- shared token file: `/run/vault/token`
|
||||||
|
- app reads token via `VAULT_TOKEN_FILE=/run/vault/token` and `token_env: VAULT_TOKEN`
|
||||||
|
- Prod compose:
|
||||||
|
- service: `sendico_callbacks_vault_agent`
|
||||||
|
- same token sink and env flow
|
||||||
|
- AppRole creds are injected at deploy from `CALLBACKS_VAULT_SECRET_PATH` (default `sendico/edge/callbacks/vault`)
|
||||||
|
|
||||||
|
Required Vault policy (minimal read-only for KV v2 mount `kv`):
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
path "kv/data/callbacks/*" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "kv/metadata/callbacks/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create policy + role (example):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vault policy write callbacks callbacks-policy.hcl
|
||||||
|
vault write auth/approle/role/callbacks \
|
||||||
|
token_policies="callbacks" \
|
||||||
|
token_ttl="1h" \
|
||||||
|
token_max_ttl="24h"
|
||||||
|
vault read -field=role_id auth/approle/role/callbacks/role-id
|
||||||
|
vault write -f -field=secret_id auth/approle/role/callbacks/secret-id
|
||||||
|
```
|
||||||
|
|
||||||
|
Store AppRole creds for prod deploy pipeline:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vault kv put kv/sendico/edge/callbacks/vault \
|
||||||
|
role_id="<callbacks-role-id>" \
|
||||||
|
secret_id="<callbacks-secret-id>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Store webhook signing secrets (example path consumed by `secret_ref`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vault kv put kv/callbacks/client-a/webhook secret="super-secret"
|
||||||
|
```
|
||||||
|
|||||||
46
api/edge/callbacks/.air.toml
Normal file
46
api/edge/callbacks/.air.toml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
entrypoint = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go", "_templ.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
3
api/edge/callbacks/.gitignore
vendored
Normal file
3
api/edge/callbacks/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
app
|
||||||
|
.gocache
|
||||||
|
tmp
|
||||||
64
api/edge/callbacks/config.dev.yml
Normal file
64
api/edge/callbacks/config.dev.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
runtime:
|
||||||
|
shutdown_timeout_seconds: 15
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
address: ":9420"
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: mongodb
|
||||||
|
settings:
|
||||||
|
host_env: CALLBACKS_MONGO_HOST
|
||||||
|
port_env: CALLBACKS_MONGO_PORT
|
||||||
|
database_env: CALLBACKS_MONGO_DATABASE
|
||||||
|
user_env: CALLBACKS_MONGO_USER
|
||||||
|
password_env: CALLBACKS_MONGO_PASSWORD
|
||||||
|
auth_source_env: CALLBACKS_MONGO_AUTH_SOURCE
|
||||||
|
replica_set_env: CALLBACKS_MONGO_REPLICA_SET
|
||||||
|
|
||||||
|
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: Edge Callbacks Service
|
||||||
|
max_reconnects: 10
|
||||||
|
reconnect_wait: 5
|
||||||
|
buffer_size: 1024
|
||||||
|
|
||||||
|
ingest:
|
||||||
|
stream: CALLBACKS
|
||||||
|
subject: callbacks.events
|
||||||
|
durable: callbacks-ingest
|
||||||
|
batch_size: 32
|
||||||
|
fetch_timeout_ms: 2000
|
||||||
|
idle_sleep_ms: 500
|
||||||
|
|
||||||
|
|
||||||
|
delivery:
|
||||||
|
worker_concurrency: 8
|
||||||
|
worker_poll_ms: 200
|
||||||
|
lock_ttl_seconds: 30
|
||||||
|
request_timeout_ms: 10000
|
||||||
|
max_attempts: 8
|
||||||
|
min_delay_ms: 1000
|
||||||
|
max_delay_ms: 300000
|
||||||
|
jitter_ratio: 0.2
|
||||||
|
|
||||||
|
security:
|
||||||
|
require_https: true
|
||||||
|
allowed_hosts: []
|
||||||
|
allowed_ports: [443]
|
||||||
|
dns_resolve_timeout_ms: 2000
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
cache_ttl_seconds: 60
|
||||||
|
static: {}
|
||||||
|
vault:
|
||||||
|
address: "http://dev-vault:8200"
|
||||||
|
token_env: VAULT_TOKEN
|
||||||
|
namespace: ""
|
||||||
|
mount_path: kv
|
||||||
|
default_field: value
|
||||||
63
api/edge/callbacks/config.yml
Normal file
63
api/edge/callbacks/config.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
runtime:
|
||||||
|
shutdown_timeout_seconds: 15
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
address: ":9420"
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: mongodb
|
||||||
|
settings:
|
||||||
|
host_env: CALLBACKS_MONGO_HOST
|
||||||
|
port_env: CALLBACKS_MONGO_PORT
|
||||||
|
database_env: CALLBACKS_MONGO_DATABASE
|
||||||
|
user_env: CALLBACKS_MONGO_USER
|
||||||
|
password_env: CALLBACKS_MONGO_PASSWORD
|
||||||
|
auth_source_env: CALLBACKS_MONGO_AUTH_SOURCE
|
||||||
|
replica_set_env: CALLBACKS_MONGO_REPLICA_SET
|
||||||
|
|
||||||
|
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: Edge Callbacks Service
|
||||||
|
max_reconnects: 10
|
||||||
|
reconnect_wait: 5
|
||||||
|
buffer_size: 1024
|
||||||
|
|
||||||
|
ingest:
|
||||||
|
stream: CALLBACKS
|
||||||
|
subject: callbacks.events
|
||||||
|
durable: callbacks-ingest
|
||||||
|
batch_size: 32
|
||||||
|
fetch_timeout_ms: 2000
|
||||||
|
idle_sleep_ms: 500
|
||||||
|
|
||||||
|
delivery:
|
||||||
|
worker_concurrency: 8
|
||||||
|
worker_poll_ms: 200
|
||||||
|
lock_ttl_seconds: 30
|
||||||
|
request_timeout_ms: 10000
|
||||||
|
max_attempts: 8
|
||||||
|
min_delay_ms: 1000
|
||||||
|
max_delay_ms: 300000
|
||||||
|
jitter_ratio: 0.2
|
||||||
|
|
||||||
|
security:
|
||||||
|
require_https: true
|
||||||
|
allowed_hosts: []
|
||||||
|
allowed_ports: [443]
|
||||||
|
dns_resolve_timeout_ms: 2000
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
cache_ttl_seconds: 60
|
||||||
|
static: {}
|
||||||
|
vault:
|
||||||
|
address: "https://vault.sendico.io"
|
||||||
|
token_env: VAULT_TOKEN
|
||||||
|
namespace: ""
|
||||||
|
mount_path: kv
|
||||||
|
default_field: value
|
||||||
15
api/edge/callbacks/entrypoint.sh
Executable file
15
api/edge/callbacks/entrypoint.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ -n "${VAULT_TOKEN_FILE:-}" ] && [ -f "${VAULT_TOKEN_FILE}" ]; then
|
||||||
|
token="$(cat "${VAULT_TOKEN_FILE}" 2>/dev/null | tr -d '[:space:]')"
|
||||||
|
if [ -n "${token}" ]; then
|
||||||
|
export VAULT_TOKEN="${token}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VAULT_TOKEN:-}" ]; then
|
||||||
|
echo "[entrypoint] VAULT_TOKEN is not set; expected Vault Agent sink to write a token to ${VAULT_TOKEN_FILE:-/run/vault/token}" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
64
api/edge/callbacks/go.mod
Normal file
64
api/edge/callbacks/go.mod
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
module github.com/tech/sendico/edge/callbacks
|
||||||
|
|
||||||
|
go 1.25.7
|
||||||
|
|
||||||
|
replace github.com/tech/sendico/pkg => ../../pkg
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
|
github.com/nats-io/nats.go v1.49.0
|
||||||
|
github.com/prometheus/client_golang v1.23.2
|
||||||
|
github.com/tech/sendico/pkg v0.1.0
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.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.10.0 // indirect
|
||||||
|
github.com/casbin/casbin/v2 v2.135.0 // indirect
|
||||||
|
github.com/casbin/govaluate v1.10.0 // indirect
|
||||||
|
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||||
|
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||||
|
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||||
|
github.com/hashicorp/vault/api v1.22.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/nats-io/nkeys v0.4.15 // 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.5 // indirect
|
||||||
|
github.com/prometheus/procfs v0.20.0 // indirect
|
||||||
|
github.com/ryanuber/go-glob v1.0.0 // 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.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||||
|
google.golang.org/grpc v1.79.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
256
api/edge/callbacks/go.sum
Normal file
256
api/edge/callbacks/go.sum
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
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.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.10.0/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/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0lCi7JKrS4qQ=
|
||||||
|
github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE=
|
||||||
|
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
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.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
|
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/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
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/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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||||
|
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||||
|
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||||
|
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||||
|
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||||
|
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||||
|
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||||
|
github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
|
||||||
|
github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=
|
||||||
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
|
github.com/klauspost/compress v1.18.4/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/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
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/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.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
|
||||||
|
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
|
||||||
|
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||||
|
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||||
|
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.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
|
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
|
||||||
|
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||||
|
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/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||||
|
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
|
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/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
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.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||||
|
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
|
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=
|
||||||
28
api/edge/callbacks/internal/appversion/version.go
Normal file
28
api/edge/callbacks/internal/appversion/version.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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 {
|
||||||
|
info := version.Info{
|
||||||
|
Program: "Sendico Edge Callbacks Service",
|
||||||
|
Revision: Revision,
|
||||||
|
Branch: Branch,
|
||||||
|
BuildUser: BuildUser,
|
||||||
|
BuildDate: BuildDate,
|
||||||
|
Version: Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
return vf.Create(&info)
|
||||||
|
}
|
||||||
182
api/edge/callbacks/internal/config/module.go
Normal file
182
api/edge/callbacks/internal/config/module.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db"
|
||||||
|
"github.com/tech/sendico/pkg/messaging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultShutdownTimeoutSeconds = 15
|
||||||
|
defaultMetricsAddress = ":9420"
|
||||||
|
defaultIngestStream = "CALLBACKS"
|
||||||
|
defaultIngestSubject = "callbacks.events"
|
||||||
|
defaultIngestDurable = "callbacks-ingest"
|
||||||
|
defaultIngestBatchSize = 32
|
||||||
|
defaultIngestFetchTimeoutMS = 2000
|
||||||
|
defaultIngestIdleSleepMS = 500
|
||||||
|
defaultTaskCollection = "callback_tasks"
|
||||||
|
defaultInboxCollection = "callback_inbox"
|
||||||
|
defaultEndpointsCollection = "webhook_endpoints"
|
||||||
|
defaultWorkerConcurrency = 8
|
||||||
|
defaultWorkerPollIntervalMS = 200
|
||||||
|
defaultLockTTLSeconds = 30
|
||||||
|
defaultRequestTimeoutMS = 10000
|
||||||
|
defaultMaxAttempts = 8
|
||||||
|
defaultMinDelayMS = 1000
|
||||||
|
defaultMaxDelayMS = 300000
|
||||||
|
defaultJitterRatio = 0.20
|
||||||
|
defaultDNSResolveTimeoutMS = 2000
|
||||||
|
defaultSecretsVaultField = "value"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Loader parses callbacks service configuration.
|
||||||
|
type Loader interface {
|
||||||
|
Load(path string) (*Config, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the full callbacks service configuration.
|
||||||
|
type Config struct {
|
||||||
|
Runtime *RuntimeConfig `yaml:"runtime"`
|
||||||
|
Metrics *MetricsConfig `yaml:"metrics"`
|
||||||
|
Database *db.Config `yaml:"database"`
|
||||||
|
Messaging *messaging.Config `yaml:"messaging"`
|
||||||
|
Ingest IngestConfig `yaml:"ingest"`
|
||||||
|
Delivery DeliveryConfig `yaml:"delivery"`
|
||||||
|
Security SecurityConfig `yaml:"security"`
|
||||||
|
Secrets SecretsConfig `yaml:"secrets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuntimeConfig contains process lifecycle settings.
|
||||||
|
type RuntimeConfig struct {
|
||||||
|
ShutdownTimeoutSeconds int `yaml:"shutdown_timeout_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RuntimeConfig) ShutdownTimeout() time.Duration {
|
||||||
|
if c == nil || c.ShutdownTimeoutSeconds <= 0 {
|
||||||
|
return defaultShutdownTimeoutSeconds * time.Second
|
||||||
|
}
|
||||||
|
return time.Duration(c.ShutdownTimeoutSeconds) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetricsConfig configures observability endpoints.
|
||||||
|
type MetricsConfig struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MetricsConfig) ListenAddress() string {
|
||||||
|
if c == nil || c.Address == "" {
|
||||||
|
return defaultMetricsAddress
|
||||||
|
}
|
||||||
|
return c.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
// IngestConfig configures JetStream ingestion.
|
||||||
|
type IngestConfig struct {
|
||||||
|
Stream string `yaml:"stream"`
|
||||||
|
Subject string `yaml:"subject"`
|
||||||
|
Durable string `yaml:"durable"`
|
||||||
|
BatchSize int `yaml:"batch_size"`
|
||||||
|
FetchTimeoutMS int `yaml:"fetch_timeout_ms"`
|
||||||
|
IdleSleepMS int `yaml:"idle_sleep_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IngestConfig) FetchTimeout() time.Duration {
|
||||||
|
if c.FetchTimeoutMS <= 0 {
|
||||||
|
return time.Duration(defaultIngestFetchTimeoutMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
return time.Duration(c.FetchTimeoutMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IngestConfig) IdleSleep() time.Duration {
|
||||||
|
if c.IdleSleepMS <= 0 {
|
||||||
|
return time.Duration(defaultIngestIdleSleepMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
return time.Duration(c.IdleSleepMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeliveryConfig controls dispatcher behavior.
|
||||||
|
type DeliveryConfig struct {
|
||||||
|
WorkerConcurrency int `yaml:"worker_concurrency"`
|
||||||
|
WorkerPollMS int `yaml:"worker_poll_ms"`
|
||||||
|
LockTTLSeconds int `yaml:"lock_ttl_seconds"`
|
||||||
|
RequestTimeoutMS int `yaml:"request_timeout_ms"`
|
||||||
|
MaxAttempts int `yaml:"max_attempts"`
|
||||||
|
MinDelayMS int `yaml:"min_delay_ms"`
|
||||||
|
MaxDelayMS int `yaml:"max_delay_ms"`
|
||||||
|
JitterRatio float64 `yaml:"jitter_ratio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DeliveryConfig) WorkerPollInterval() time.Duration {
|
||||||
|
if c.WorkerPollMS <= 0 {
|
||||||
|
return time.Duration(defaultWorkerPollIntervalMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
return time.Duration(c.WorkerPollMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DeliveryConfig) LockTTL() time.Duration {
|
||||||
|
if c.LockTTLSeconds <= 0 {
|
||||||
|
return time.Duration(defaultLockTTLSeconds) * time.Second
|
||||||
|
}
|
||||||
|
return time.Duration(c.LockTTLSeconds) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DeliveryConfig) RequestTimeout() time.Duration {
|
||||||
|
if c.RequestTimeoutMS <= 0 {
|
||||||
|
return time.Duration(defaultRequestTimeoutMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
return time.Duration(c.RequestTimeoutMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DeliveryConfig) MinDelay() time.Duration {
|
||||||
|
if c.MinDelayMS <= 0 {
|
||||||
|
return time.Duration(defaultMinDelayMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
return time.Duration(c.MinDelayMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DeliveryConfig) MaxDelay() time.Duration {
|
||||||
|
if c.MaxDelayMS <= 0 {
|
||||||
|
return time.Duration(defaultMaxDelayMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
return time.Duration(c.MaxDelayMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityConfig controls outbound callback safety checks.
|
||||||
|
type SecurityConfig struct {
|
||||||
|
RequireHTTPS bool `yaml:"require_https"`
|
||||||
|
AllowedHosts []string `yaml:"allowed_hosts"`
|
||||||
|
AllowedPorts []int `yaml:"allowed_ports"`
|
||||||
|
DNSResolveTimeout int `yaml:"dns_resolve_timeout_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SecurityConfig) DNSResolveTimeoutMS() time.Duration {
|
||||||
|
if c.DNSResolveTimeout <= 0 {
|
||||||
|
return time.Duration(defaultDNSResolveTimeoutMS) * time.Millisecond
|
||||||
|
}
|
||||||
|
return time.Duration(c.DNSResolveTimeout) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretsConfig controls secret lookup behavior.
|
||||||
|
type SecretsConfig struct {
|
||||||
|
CacheTTLSeconds int `yaml:"cache_ttl_seconds"`
|
||||||
|
Static map[string]string `yaml:"static"`
|
||||||
|
Vault VaultSecretsConfig `yaml:"vault"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultSecretsConfig controls Vault KV secret resolution.
|
||||||
|
type VaultSecretsConfig struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
TokenEnv string `yaml:"token_env"`
|
||||||
|
Namespace string `yaml:"namespace"`
|
||||||
|
MountPath string `yaml:"mount_path"`
|
||||||
|
DefaultField string `yaml:"default_field"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SecretsConfig) CacheTTL() time.Duration {
|
||||||
|
if c == nil || c.CacheTTLSeconds <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return time.Duration(c.CacheTTLSeconds) * time.Second
|
||||||
|
}
|
||||||
162
api/edge/callbacks/internal/config/service.go
Normal file
162
api/edge/callbacks/internal/config/service.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a configuration loader.
|
||||||
|
func New(logger mlogger.Logger) Loader {
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
return &service{logger: logger.Named("config")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Load(path string) (*Config, error) {
|
||||||
|
if strings.TrimSpace(path) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("config path is required", "path")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to read config file", zap.String("path", path), zap.Error(err))
|
||||||
|
return nil, merrors.InternalWrap(err, "failed to read callbacks config")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||||
|
s.logger.Error("Failed to parse config yaml", zap.String("path", path), zap.Error(err))
|
||||||
|
return nil, merrors.InternalWrap(err, "failed to parse callbacks config")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.applyDefaults(cfg)
|
||||||
|
if err := s.validate(cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) applyDefaults(cfg *Config) {
|
||||||
|
if cfg.Runtime == nil {
|
||||||
|
cfg.Runtime = &RuntimeConfig{ShutdownTimeoutSeconds: defaultShutdownTimeoutSeconds}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Metrics == nil {
|
||||||
|
cfg.Metrics = &MetricsConfig{Address: defaultMetricsAddress}
|
||||||
|
} else if strings.TrimSpace(cfg.Metrics.Address) == "" {
|
||||||
|
cfg.Metrics.Address = defaultMetricsAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(cfg.Ingest.Stream) == "" {
|
||||||
|
cfg.Ingest.Stream = defaultIngestStream
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Ingest.Subject) == "" {
|
||||||
|
cfg.Ingest.Subject = defaultIngestSubject
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Ingest.Durable) == "" {
|
||||||
|
cfg.Ingest.Durable = defaultIngestDurable
|
||||||
|
}
|
||||||
|
if cfg.Ingest.BatchSize <= 0 {
|
||||||
|
cfg.Ingest.BatchSize = defaultIngestBatchSize
|
||||||
|
}
|
||||||
|
if cfg.Ingest.FetchTimeoutMS <= 0 {
|
||||||
|
cfg.Ingest.FetchTimeoutMS = defaultIngestFetchTimeoutMS
|
||||||
|
}
|
||||||
|
if cfg.Ingest.IdleSleepMS <= 0 {
|
||||||
|
cfg.Ingest.IdleSleepMS = defaultIngestIdleSleepMS
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Delivery.WorkerConcurrency <= 0 {
|
||||||
|
cfg.Delivery.WorkerConcurrency = defaultWorkerConcurrency
|
||||||
|
}
|
||||||
|
if cfg.Delivery.WorkerPollMS <= 0 {
|
||||||
|
cfg.Delivery.WorkerPollMS = defaultWorkerPollIntervalMS
|
||||||
|
}
|
||||||
|
if cfg.Delivery.LockTTLSeconds <= 0 {
|
||||||
|
cfg.Delivery.LockTTLSeconds = defaultLockTTLSeconds
|
||||||
|
}
|
||||||
|
if cfg.Delivery.RequestTimeoutMS <= 0 {
|
||||||
|
cfg.Delivery.RequestTimeoutMS = defaultRequestTimeoutMS
|
||||||
|
}
|
||||||
|
if cfg.Delivery.MaxAttempts <= 0 {
|
||||||
|
cfg.Delivery.MaxAttempts = defaultMaxAttempts
|
||||||
|
}
|
||||||
|
if cfg.Delivery.MinDelayMS <= 0 {
|
||||||
|
cfg.Delivery.MinDelayMS = defaultMinDelayMS
|
||||||
|
}
|
||||||
|
if cfg.Delivery.MaxDelayMS <= 0 {
|
||||||
|
cfg.Delivery.MaxDelayMS = defaultMaxDelayMS
|
||||||
|
}
|
||||||
|
if cfg.Delivery.JitterRatio <= 0 {
|
||||||
|
cfg.Delivery.JitterRatio = defaultJitterRatio
|
||||||
|
}
|
||||||
|
if cfg.Delivery.JitterRatio > 1 {
|
||||||
|
cfg.Delivery.JitterRatio = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Security.DNSResolveTimeout <= 0 {
|
||||||
|
cfg.Security.DNSResolveTimeout = defaultDNSResolveTimeoutMS
|
||||||
|
}
|
||||||
|
if len(cfg.Security.AllowedPorts) == 0 {
|
||||||
|
cfg.Security.AllowedPorts = []int{443}
|
||||||
|
}
|
||||||
|
if !cfg.Security.RequireHTTPS {
|
||||||
|
cfg.Security.RequireHTTPS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Secrets.Static == nil {
|
||||||
|
cfg.Secrets.Static = map[string]string{}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Secrets.Vault.DefaultField) == "" {
|
||||||
|
cfg.Secrets.Vault.DefaultField = defaultSecretsVaultField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) validate(cfg *Config) error {
|
||||||
|
if cfg.Database == nil {
|
||||||
|
return merrors.InvalidArgument("database configuration is required", "database")
|
||||||
|
}
|
||||||
|
if cfg.Messaging == nil {
|
||||||
|
return merrors.InvalidArgument("messaging configuration is required", "messaging")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(cfg.Messaging.Driver)) == "" {
|
||||||
|
return merrors.InvalidArgument("messaging.driver is required", "messaging.driver")
|
||||||
|
}
|
||||||
|
if cfg.Delivery.MinDelay() > cfg.Delivery.MaxDelay() {
|
||||||
|
return merrors.InvalidArgument("delivery min delay must be <= max delay", "delivery.min_delay_ms", "delivery.max_delay_ms")
|
||||||
|
}
|
||||||
|
if cfg.Delivery.MaxAttempts < 1 {
|
||||||
|
return merrors.InvalidArgument("delivery.max_attempts must be > 0", "delivery.max_attempts")
|
||||||
|
}
|
||||||
|
if cfg.Ingest.BatchSize < 1 {
|
||||||
|
return merrors.InvalidArgument("ingest.batch_size must be > 0", "ingest.batch_size")
|
||||||
|
}
|
||||||
|
vaultAddress := strings.TrimSpace(cfg.Secrets.Vault.Address)
|
||||||
|
vaultTokenEnv := strings.TrimSpace(cfg.Secrets.Vault.TokenEnv)
|
||||||
|
vaultMountPath := strings.TrimSpace(cfg.Secrets.Vault.MountPath)
|
||||||
|
hasVault := vaultAddress != "" || vaultTokenEnv != "" || vaultMountPath != ""
|
||||||
|
if hasVault {
|
||||||
|
if vaultAddress == "" {
|
||||||
|
return merrors.InvalidArgument("secrets.vault.address is required when vault settings are configured", "secrets.vault.address")
|
||||||
|
}
|
||||||
|
if vaultTokenEnv == "" {
|
||||||
|
return merrors.InvalidArgument("secrets.vault.token_env is required when vault settings are configured", "secrets.vault.token_env")
|
||||||
|
}
|
||||||
|
if vaultMountPath == "" {
|
||||||
|
return merrors.InvalidArgument("secrets.vault.mount_path is required when vault settings are configured", "secrets.vault.mount_path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
27
api/edge/callbacks/internal/delivery/classifier.go
Normal file
27
api/edge/callbacks/internal/delivery/classifier.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package delivery
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type outcome string
|
||||||
|
|
||||||
|
const (
|
||||||
|
outcomeDelivered outcome = "delivered"
|
||||||
|
outcomeRetry outcome = "retry"
|
||||||
|
outcomeFailed outcome = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func classify(statusCode int, reqErr error) outcome {
|
||||||
|
if reqErr != nil {
|
||||||
|
return outcomeRetry
|
||||||
|
}
|
||||||
|
if statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices {
|
||||||
|
return outcomeDelivered
|
||||||
|
}
|
||||||
|
if statusCode == http.StatusTooManyRequests || statusCode == http.StatusRequestTimeout {
|
||||||
|
return outcomeRetry
|
||||||
|
}
|
||||||
|
if statusCode >= http.StatusInternalServerError {
|
||||||
|
return outcomeRetry
|
||||||
|
}
|
||||||
|
return outcomeFailed
|
||||||
|
}
|
||||||
48
api/edge/callbacks/internal/delivery/module.go
Normal file
48
api/edge/callbacks/internal/delivery/module.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package delivery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/retry"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/security"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/signing"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/storage"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Observer captures delivery metrics.
|
||||||
|
type Observer interface {
|
||||||
|
ObserveDelivery(result string, statusCode int, duration time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config controls delivery worker runtime.
|
||||||
|
type Config struct {
|
||||||
|
WorkerConcurrency int
|
||||||
|
WorkerPoll time.Duration
|
||||||
|
LockTTL time.Duration
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
JitterRatio float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies configure delivery dispatcher.
|
||||||
|
type Dependencies struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Config Config
|
||||||
|
Tasks storage.TaskRepo
|
||||||
|
Retry retry.Policy
|
||||||
|
Security security.Validator
|
||||||
|
Signer signing.Signer
|
||||||
|
Observer Observer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service executes callback delivery tasks.
|
||||||
|
type Service interface {
|
||||||
|
Start(ctx context.Context)
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates delivery service.
|
||||||
|
func New(deps Dependencies) (Service, error) {
|
||||||
|
return newService(deps)
|
||||||
|
}
|
||||||
263
api/edge/callbacks/internal/delivery/service.go
Normal file
263
api/edge/callbacks/internal/delivery/service.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package delivery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/signing"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/storage"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const responseDrainLimit = 64 * 1024
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
cfg Config
|
||||||
|
tasks storage.TaskRepo
|
||||||
|
retry interface {
|
||||||
|
NextAttempt(attempt int, now time.Time, minDelay, maxDelay time.Duration, jitterRatio float64) time.Time
|
||||||
|
}
|
||||||
|
security interface {
|
||||||
|
ValidateURL(ctx context.Context, target string) error
|
||||||
|
}
|
||||||
|
signer signing.Signer
|
||||||
|
obs Observer
|
||||||
|
client *http.Client
|
||||||
|
|
||||||
|
cancel context.CancelFunc
|
||||||
|
once sync.Once
|
||||||
|
stop sync.Once
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func newService(deps Dependencies) (Service, error) {
|
||||||
|
if deps.Tasks == nil {
|
||||||
|
return nil, merrors.InvalidArgument("delivery: task repo is required", "tasks")
|
||||||
|
}
|
||||||
|
if deps.Retry == nil {
|
||||||
|
return nil, merrors.InvalidArgument("delivery: retry policy is required", "retry")
|
||||||
|
}
|
||||||
|
if deps.Security == nil {
|
||||||
|
return nil, merrors.InvalidArgument("delivery: security validator is required", "security")
|
||||||
|
}
|
||||||
|
if deps.Signer == nil {
|
||||||
|
return nil, merrors.InvalidArgument("delivery: signer is required", "signer")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := deps.Logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := deps.Config
|
||||||
|
if cfg.WorkerConcurrency <= 0 {
|
||||||
|
cfg.WorkerConcurrency = 1
|
||||||
|
}
|
||||||
|
if cfg.WorkerPoll <= 0 {
|
||||||
|
cfg.WorkerPoll = 200 * time.Millisecond
|
||||||
|
}
|
||||||
|
if cfg.LockTTL <= 0 {
|
||||||
|
cfg.LockTTL = 30 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.RequestTimeout <= 0 {
|
||||||
|
cfg.RequestTimeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
MaxIdleConns: 200,
|
||||||
|
MaxIdleConnsPerHost: 32,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
|
ExpectContinueTimeout: time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service{
|
||||||
|
logger: logger.Named("delivery"),
|
||||||
|
cfg: cfg,
|
||||||
|
tasks: deps.Tasks,
|
||||||
|
retry: deps.Retry,
|
||||||
|
security: deps.Security,
|
||||||
|
signer: deps.Signer,
|
||||||
|
obs: deps.Observer,
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Start(ctx context.Context) {
|
||||||
|
s.once.Do(func() {
|
||||||
|
runCtx := ctx
|
||||||
|
if runCtx == nil {
|
||||||
|
runCtx = context.Background()
|
||||||
|
}
|
||||||
|
runCtx, s.cancel = context.WithCancel(runCtx)
|
||||||
|
|
||||||
|
for i := 0; i < s.cfg.WorkerConcurrency; i++ {
|
||||||
|
workerID := "worker-" + strconv.Itoa(i+1)
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func(id string) {
|
||||||
|
defer s.wg.Done()
|
||||||
|
s.runWorker(runCtx, id)
|
||||||
|
}(workerID)
|
||||||
|
}
|
||||||
|
s.logger.Info("Delivery workers started", zap.Int("workers", s.cfg.WorkerConcurrency))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Stop() {
|
||||||
|
s.stop.Do(func() {
|
||||||
|
if s.cancel != nil {
|
||||||
|
s.cancel()
|
||||||
|
}
|
||||||
|
s.wg.Wait()
|
||||||
|
s.logger.Info("Delivery workers stopped")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) runWorker(ctx context.Context, workerID string) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
task, err := s.tasks.LockNextTask(ctx, now, workerID, s.cfg.LockTTL)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to lock next task", zap.String("worker_id", workerID), zap.Error(err))
|
||||||
|
time.Sleep(s.cfg.WorkerPoll)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if task == nil {
|
||||||
|
time.Sleep(s.cfg.WorkerPoll)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.handleTask(ctx, workerID, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) handleTask(ctx context.Context, workerID string, task *storage.Task) {
|
||||||
|
started := time.Now()
|
||||||
|
statusCode := 0
|
||||||
|
result := "failed"
|
||||||
|
attempt := task.Attempt + 1
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if s.obs != nil {
|
||||||
|
s.obs.ObserveDelivery(result, statusCode, time.Since(started))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := s.security.ValidateURL(ctx, task.EndpointURL); err != nil {
|
||||||
|
result = "blocked"
|
||||||
|
_ = s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := task.RequestTimeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = s.cfg.RequestTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := s.signer.Sign(ctx, task.SigningMode, task.SecretRef, task.Payload, time.Now().UTC())
|
||||||
|
if err != nil {
|
||||||
|
result = "sign_error"
|
||||||
|
_ = s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, task.EndpointURL, bytes.NewReader(signed.Body))
|
||||||
|
if err != nil {
|
||||||
|
result = "request_error"
|
||||||
|
_ = s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
for key, val := range task.Headers {
|
||||||
|
req.Header.Set(key, val)
|
||||||
|
}
|
||||||
|
for key, val := range signed.Headers {
|
||||||
|
req.Header.Set(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, reqErr := s.client.Do(req)
|
||||||
|
if resp != nil {
|
||||||
|
statusCode = resp.StatusCode
|
||||||
|
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, responseDrainLimit))
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
out := classify(statusCode, reqErr)
|
||||||
|
now := time.Now().UTC()
|
||||||
|
switch out {
|
||||||
|
case outcomeDelivered:
|
||||||
|
result = string(outcomeDelivered)
|
||||||
|
if err := s.tasks.MarkDelivered(ctx, task.ID, statusCode, time.Since(started), now); err != nil {
|
||||||
|
s.logger.Warn("Failed to mark task delivered", zap.String("worker_id", workerID), zap.String("task_id", task.ID.Hex()), zap.Error(err))
|
||||||
|
}
|
||||||
|
case outcomeRetry:
|
||||||
|
if attempt < task.MaxAttempts {
|
||||||
|
next := s.retry.NextAttempt(attempt, now, task.MinDelay, task.MaxDelay, s.cfg.JitterRatio)
|
||||||
|
result = string(outcomeRetry)
|
||||||
|
lastErr := stringifyErr(reqErr)
|
||||||
|
if reqErr == nil && statusCode > 0 {
|
||||||
|
lastErr = "upstream returned retryable status"
|
||||||
|
}
|
||||||
|
if err := s.tasks.MarkRetry(ctx, task.ID, attempt, next, lastErr, statusCode, now); err != nil {
|
||||||
|
s.logger.Warn("Failed to mark task retry", zap.String("worker_id", workerID), zap.String("task_id", task.ID.Hex()), zap.Error(err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = string(outcomeFailed)
|
||||||
|
lastErr := stringifyErr(reqErr)
|
||||||
|
if reqErr == nil && statusCode > 0 {
|
||||||
|
lastErr = "upstream returned retryable status but max attempts reached"
|
||||||
|
}
|
||||||
|
if err := s.tasks.MarkFailed(ctx, task.ID, attempt, lastErr, statusCode, now); err != nil {
|
||||||
|
s.logger.Warn("Failed to mark task failed", zap.String("worker_id", workerID), zap.String("task_id", task.ID.Hex()), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = string(outcomeFailed)
|
||||||
|
lastErr := stringifyErr(reqErr)
|
||||||
|
if reqErr == nil && statusCode > 0 {
|
||||||
|
lastErr = "upstream returned non-retryable status"
|
||||||
|
}
|
||||||
|
if err := s.tasks.MarkFailed(ctx, task.ID, attempt, lastErr, statusCode, now); err != nil {
|
||||||
|
s.logger.Warn("Failed to mark task failed", zap.String("worker_id", workerID), zap.String("task_id", task.ID.Hex()), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyErr(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return "request canceled"
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return "request timeout"
|
||||||
|
}
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
33
api/edge/callbacks/internal/events/module.go
Normal file
33
api/edge/callbacks/internal/events/module.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Envelope is the canonical incoming event envelope.
|
||||||
|
type Envelope struct {
|
||||||
|
EventID string `json:"event_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
OccurredAt time.Time `json:"occurred_at"`
|
||||||
|
PublishedAt time.Time `json:"published_at,omitempty"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service parses incoming messages and builds outbound payload bytes.
|
||||||
|
type Service interface {
|
||||||
|
Parse(data []byte) (*Envelope, error)
|
||||||
|
BuildPayload(ctx context.Context, envelope *Envelope) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload is the stable outbound JSON body.
|
||||||
|
type Payload struct {
|
||||||
|
EventID string `json:"event_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
OccurredAt string `json:"occurred_at"`
|
||||||
|
PublishedAt string `json:"published_at,omitempty"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
86
api/edge/callbacks/internal/events/service.go
Normal file
86
api/edge/callbacks/internal/events/service.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type parserService struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates event parser/payload builder service.
|
||||||
|
func New(logger mlogger.Logger) Service {
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
return &parserService{logger: logger.Named("events")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *parserService) Parse(data []byte) (*Envelope, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, merrors.InvalidArgument("event payload is empty", "data")
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelope Envelope
|
||||||
|
if err := json.Unmarshal(data, &envelope); err != nil {
|
||||||
|
return nil, merrors.InvalidArgumentWrap(err, "event payload is not valid JSON", "data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(envelope.EventID) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("event_id is required", "event_id")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(envelope.Type) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("type is required", "type")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(envelope.ClientID) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("client_id is required", "client_id")
|
||||||
|
}
|
||||||
|
if envelope.OccurredAt.IsZero() {
|
||||||
|
return nil, merrors.InvalidArgument("occurred_at is required", "occurred_at")
|
||||||
|
}
|
||||||
|
if len(envelope.Data) == 0 {
|
||||||
|
envelope.Data = []byte("{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
envelope.EventID = strings.TrimSpace(envelope.EventID)
|
||||||
|
envelope.Type = strings.TrimSpace(envelope.Type)
|
||||||
|
envelope.ClientID = strings.TrimSpace(envelope.ClientID)
|
||||||
|
envelope.OccurredAt = envelope.OccurredAt.UTC()
|
||||||
|
if !envelope.PublishedAt.IsZero() {
|
||||||
|
envelope.PublishedAt = envelope.PublishedAt.UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &envelope, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *parserService) BuildPayload(_ context.Context, envelope *Envelope) ([]byte, error) {
|
||||||
|
if envelope == nil {
|
||||||
|
return nil, merrors.InvalidArgument("event envelope is required", "envelope")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := Payload{
|
||||||
|
EventID: envelope.EventID,
|
||||||
|
Type: envelope.Type,
|
||||||
|
ClientID: envelope.ClientID,
|
||||||
|
OccurredAt: envelope.OccurredAt.UTC().Format(time.RFC3339Nano),
|
||||||
|
Data: envelope.Data,
|
||||||
|
}
|
||||||
|
if !envelope.PublishedAt.IsZero() {
|
||||||
|
payload.PublishedAt = envelope.PublishedAt.UTC().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to marshal callback payload", zap.Error(err), zap.String("event_id", envelope.EventID))
|
||||||
|
return nil, merrors.InternalWrap(err, "failed to marshal callback payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
51
api/edge/callbacks/internal/ingest/module.go
Normal file
51
api/edge/callbacks/internal/ingest/module.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package ingest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/events"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/storage"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/subscriptions"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Observer captures ingest metrics.
|
||||||
|
type Observer interface {
|
||||||
|
ObserveIngest(result string, duration time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config contains JetStream ingest settings.
|
||||||
|
type Config struct {
|
||||||
|
Stream string
|
||||||
|
Subject string
|
||||||
|
Durable string
|
||||||
|
BatchSize int
|
||||||
|
FetchTimeout time.Duration
|
||||||
|
IdleSleep time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies configure the ingest service.
|
||||||
|
type Dependencies struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
JetStream nats.JetStreamContext
|
||||||
|
Config Config
|
||||||
|
Events events.Service
|
||||||
|
Resolver subscriptions.Resolver
|
||||||
|
InboxRepo storage.InboxRepo
|
||||||
|
TaskRepo storage.TaskRepo
|
||||||
|
TaskDefaults storage.TaskDefaults
|
||||||
|
Observer Observer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service runs JetStream ingest workers.
|
||||||
|
type Service interface {
|
||||||
|
Start(ctx context.Context)
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates ingest service.
|
||||||
|
func New(deps Dependencies) (Service, error) {
|
||||||
|
return newService(deps)
|
||||||
|
}
|
||||||
204
api/edge/callbacks/internal/ingest/service.go
Normal file
204
api/edge/callbacks/internal/ingest/service.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package ingest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
js nats.JetStreamContext
|
||||||
|
cfg Config
|
||||||
|
deps Dependencies
|
||||||
|
|
||||||
|
cancel context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
|
once sync.Once
|
||||||
|
stop sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newService(deps Dependencies) (Service, error) {
|
||||||
|
if deps.JetStream == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ingest: jetstream context is required", "jetstream")
|
||||||
|
}
|
||||||
|
if deps.Events == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ingest: events service is required", "events")
|
||||||
|
}
|
||||||
|
if deps.Resolver == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ingest: subscriptions resolver is required", "resolver")
|
||||||
|
}
|
||||||
|
if deps.InboxRepo == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ingest: inbox repo is required", "inboxRepo")
|
||||||
|
}
|
||||||
|
if deps.TaskRepo == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ingest: task repo is required", "taskRepo")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(deps.Config.Subject) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("ingest: subject is required", "config.subject")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(deps.Config.Durable) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("ingest: durable is required", "config.durable")
|
||||||
|
}
|
||||||
|
if deps.Config.BatchSize <= 0 {
|
||||||
|
deps.Config.BatchSize = 1
|
||||||
|
}
|
||||||
|
if deps.Config.FetchTimeout <= 0 {
|
||||||
|
deps.Config.FetchTimeout = 2 * time.Second
|
||||||
|
}
|
||||||
|
if deps.Config.IdleSleep <= 0 {
|
||||||
|
deps.Config.IdleSleep = 500 * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := deps.Logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service{
|
||||||
|
logger: logger.Named("ingest"),
|
||||||
|
js: deps.JetStream,
|
||||||
|
cfg: deps.Config,
|
||||||
|
deps: deps,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Start(ctx context.Context) {
|
||||||
|
s.once.Do(func() {
|
||||||
|
runCtx := ctx
|
||||||
|
if runCtx == nil {
|
||||||
|
runCtx = context.Background()
|
||||||
|
}
|
||||||
|
runCtx, s.cancel = context.WithCancel(runCtx)
|
||||||
|
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer s.wg.Done()
|
||||||
|
s.run(runCtx)
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Stop() {
|
||||||
|
s.stop.Do(func() {
|
||||||
|
if s.cancel != nil {
|
||||||
|
s.cancel()
|
||||||
|
}
|
||||||
|
s.wg.Wait()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) run(ctx context.Context) {
|
||||||
|
subOpts := []nats.SubOpt{}
|
||||||
|
if stream := strings.TrimSpace(s.cfg.Stream); stream != "" {
|
||||||
|
subOpts = append(subOpts, nats.BindStream(stream))
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := s.js.PullSubscribe(strings.TrimSpace(s.cfg.Subject), strings.TrimSpace(s.cfg.Durable), subOpts...)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to start JetStream subscription", zap.String("subject", s.cfg.Subject), zap.String("durable", s.cfg.Durable), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Ingest consumer started", zap.String("subject", s.cfg.Subject), zap.String("durable", s.cfg.Durable), zap.Int("batch_size", s.cfg.BatchSize))
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
s.logger.Info("Ingest consumer stopped")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs, err := sub.Fetch(s.cfg.BatchSize, nats.MaxWait(s.cfg.FetchTimeout))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, nats.ErrTimeout) {
|
||||||
|
time.Sleep(s.cfg.IdleSleep)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.Warn("Failed to fetch JetStream messages", zap.Error(err))
|
||||||
|
time.Sleep(s.cfg.IdleSleep)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, msg := range msgs {
|
||||||
|
s.handleMessage(ctx, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) handleMessage(ctx context.Context, msg *nats.Msg) {
|
||||||
|
start := time.Now()
|
||||||
|
result := "ok"
|
||||||
|
nak := false
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if s.deps.Observer != nil {
|
||||||
|
s.deps.Observer.ObserveIngest(result, time.Since(start))
|
||||||
|
}
|
||||||
|
|
||||||
|
var ackErr error
|
||||||
|
if nak {
|
||||||
|
ackErr = msg.Nak()
|
||||||
|
} else {
|
||||||
|
ackErr = msg.Ack()
|
||||||
|
}
|
||||||
|
if ackErr != nil {
|
||||||
|
s.logger.Warn("Failed to ack ingest message", zap.Bool("nak", nak), zap.Error(ackErr))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
envelope, err := s.deps.Events.Parse(msg.Data)
|
||||||
|
if err != nil {
|
||||||
|
result = "invalid_event"
|
||||||
|
nak = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inserted, err := s.deps.InboxRepo.TryInsert(ctx, envelope.EventID, envelope.ClientID, envelope.Type, time.Now().UTC())
|
||||||
|
if err != nil {
|
||||||
|
result = "inbox_error"
|
||||||
|
nak = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !inserted {
|
||||||
|
result = "duplicate"
|
||||||
|
nak = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := s.deps.Resolver.Resolve(ctx, envelope.ClientID, envelope.Type)
|
||||||
|
if err != nil {
|
||||||
|
result = "resolve_error"
|
||||||
|
nak = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(endpoints) == 0 {
|
||||||
|
result = "no_endpoints"
|
||||||
|
nak = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := s.deps.Events.BuildPayload(ctx, envelope)
|
||||||
|
if err != nil {
|
||||||
|
result = "payload_error"
|
||||||
|
nak = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.deps.TaskRepo.UpsertTasks(ctx, envelope.EventID, endpoints, payload, s.deps.TaskDefaults, time.Now().UTC()); err != nil {
|
||||||
|
result = "task_error"
|
||||||
|
nak = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
36
api/edge/callbacks/internal/ops/module.go
Normal file
36
api/edge/callbacks/internal/ops/module.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package ops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/health"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Observer records service metrics.
|
||||||
|
type Observer interface {
|
||||||
|
ObserveIngest(result string, duration time.Duration)
|
||||||
|
ObserveDelivery(result string, statusCode int, duration time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPServer exposes /metrics and /health.
|
||||||
|
type HTTPServer interface {
|
||||||
|
SetStatus(status health.ServiceStatus)
|
||||||
|
Close(ctx context.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPServerConfig configures observability endpoint.
|
||||||
|
type HTTPServerConfig struct {
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewObserver creates process metrics observer.
|
||||||
|
func NewObserver() Observer {
|
||||||
|
return newObserver()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPServer creates observability HTTP server.
|
||||||
|
func NewHTTPServer(logger mlogger.Logger, cfg HTTPServerConfig) (HTTPServer, error) {
|
||||||
|
return newHTTPServer(logger, cfg)
|
||||||
|
}
|
||||||
119
api/edge/callbacks/internal/ops/server.go
Normal file
119
api/edge/callbacks/internal/ops/server.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package ops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"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"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultAddress = ":9420"
|
||||||
|
readHeaderTimeout = 5 * time.Second
|
||||||
|
defaultShutdownWindow = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpServer struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
server *http.Server
|
||||||
|
health routers.Health
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPServer(logger mlogger.Logger, cfg HTTPServerConfig) (HTTPServer, error) {
|
||||||
|
if logger == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ops: logger is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
address := strings.TrimSpace(cfg.Address)
|
||||||
|
if address == "" {
|
||||||
|
address = defaultAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
|
metricsLogger := logger.Named("ops")
|
||||||
|
var healthRouter routers.Health
|
||||||
|
hr, err := routers.NewHealthRouter(metricsLogger, r, "")
|
||||||
|
if err != nil {
|
||||||
|
metricsLogger.Warn("Failed to initialise health router", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
hr.SetStatus(health.SSStarting)
|
||||||
|
healthRouter = hr
|
||||||
|
}
|
||||||
|
|
||||||
|
httpSrv := &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
Handler: r,
|
||||||
|
ReadHeaderTimeout: readHeaderTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper := &httpServer{
|
||||||
|
logger: metricsLogger,
|
||||||
|
server: httpSrv,
|
||||||
|
health: healthRouter,
|
||||||
|
timeout: defaultShutdownWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
metricsLogger.Info("Prometheus endpoint listening", zap.String("address", address))
|
||||||
|
serveErr := httpSrv.ListenAndServe()
|
||||||
|
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
|
||||||
|
metricsLogger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(serveErr))
|
||||||
|
if healthRouter != nil {
|
||||||
|
healthRouter.SetStatus(health.SSTerminating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return wrapper, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpServer) SetStatus(status health.ServiceStatus) {
|
||||||
|
if s == nil || s.health == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.health.SetStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpServer) Close(ctx context.Context) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.health != nil {
|
||||||
|
s.health.SetStatus(health.SSTerminating)
|
||||||
|
s.health.Finish()
|
||||||
|
s.health = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.server == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdownCtx := ctx
|
||||||
|
if shutdownCtx == nil {
|
||||||
|
shutdownCtx = context.Background()
|
||||||
|
}
|
||||||
|
if s.timeout > 0 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
shutdownCtx, cancel = context.WithTimeout(shutdownCtx, s.timeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.server.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
s.logger.Warn("Failed to stop metrics server", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
s.logger.Info("Metrics server stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
75
api/edge/callbacks/internal/ops/service.go
Normal file
75
api/edge/callbacks/internal/ops/service.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package ops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
metricsOnce sync.Once
|
||||||
|
ingestTotal *prometheus.CounterVec
|
||||||
|
ingestLatency *prometheus.HistogramVec
|
||||||
|
deliveryTotal *prometheus.CounterVec
|
||||||
|
deliveryLatency *prometheus.HistogramVec
|
||||||
|
)
|
||||||
|
|
||||||
|
type observer struct{}
|
||||||
|
|
||||||
|
func newObserver() Observer {
|
||||||
|
initMetrics()
|
||||||
|
return observer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initMetrics() {
|
||||||
|
metricsOnce.Do(func() {
|
||||||
|
ingestTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "sendico",
|
||||||
|
Subsystem: "callbacks",
|
||||||
|
Name: "ingest_total",
|
||||||
|
Help: "Total ingest attempts by result",
|
||||||
|
}, []string{"result"})
|
||||||
|
|
||||||
|
ingestLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Namespace: "sendico",
|
||||||
|
Subsystem: "callbacks",
|
||||||
|
Name: "ingest_duration_seconds",
|
||||||
|
Help: "Ingest latency in seconds",
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
}, []string{"result"})
|
||||||
|
|
||||||
|
deliveryTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "sendico",
|
||||||
|
Subsystem: "callbacks",
|
||||||
|
Name: "delivery_total",
|
||||||
|
Help: "Total delivery attempts by result and status code",
|
||||||
|
}, []string{"result", "status_code"})
|
||||||
|
|
||||||
|
deliveryLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Namespace: "sendico",
|
||||||
|
Subsystem: "callbacks",
|
||||||
|
Name: "delivery_duration_seconds",
|
||||||
|
Help: "Delivery latency in seconds",
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
}, []string{"result"})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (observer) ObserveIngest(result string, duration time.Duration) {
|
||||||
|
if result == "" {
|
||||||
|
result = "unknown"
|
||||||
|
}
|
||||||
|
ingestTotal.WithLabelValues(result).Inc()
|
||||||
|
ingestLatency.WithLabelValues(result).Observe(duration.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (observer) ObserveDelivery(result string, statusCode int, duration time.Duration) {
|
||||||
|
if result == "" {
|
||||||
|
result = "unknown"
|
||||||
|
}
|
||||||
|
deliveryTotal.WithLabelValues(result, strconv.Itoa(statusCode)).Inc()
|
||||||
|
deliveryLatency.WithLabelValues(result).Observe(duration.Seconds())
|
||||||
|
}
|
||||||
8
api/edge/callbacks/internal/retry/module.go
Normal file
8
api/edge/callbacks/internal/retry/module.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package retry
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Policy computes retry schedules.
|
||||||
|
type Policy interface {
|
||||||
|
NextAttempt(attempt int, now time.Time, minDelay, maxDelay time.Duration, jitterRatio float64) time.Time
|
||||||
|
}
|
||||||
59
api/edge/callbacks/internal/retry/service.go
Normal file
59
api/edge/callbacks/internal/retry/service.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package retry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
rnd *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates retry policy service.
|
||||||
|
func New() Policy {
|
||||||
|
return &service{rnd: rand.New(rand.NewSource(time.Now().UnixNano()))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) NextAttempt(attempt int, now time.Time, minDelay, maxDelay time.Duration, jitterRatio float64) time.Time {
|
||||||
|
if attempt < 1 {
|
||||||
|
attempt = 1
|
||||||
|
}
|
||||||
|
if minDelay <= 0 {
|
||||||
|
minDelay = time.Second
|
||||||
|
}
|
||||||
|
if maxDelay < minDelay {
|
||||||
|
maxDelay = minDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
base := float64(minDelay)
|
||||||
|
delay := time.Duration(base * math.Pow(2, float64(attempt-1)))
|
||||||
|
if delay > maxDelay {
|
||||||
|
delay = maxDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
if jitterRatio > 0 {
|
||||||
|
if jitterRatio > 1 {
|
||||||
|
jitterRatio = 1
|
||||||
|
}
|
||||||
|
maxJitter := int64(float64(delay) * jitterRatio)
|
||||||
|
if maxJitter > 0 {
|
||||||
|
s.mu.Lock()
|
||||||
|
jitter := s.rnd.Int63n((maxJitter * 2) + 1)
|
||||||
|
s.mu.Unlock()
|
||||||
|
delta := jitter - maxJitter
|
||||||
|
delay += time.Duration(delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if delay < minDelay {
|
||||||
|
delay = minDelay
|
||||||
|
}
|
||||||
|
if delay > maxDelay {
|
||||||
|
delay = maxDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
return now.UTC().Add(delay)
|
||||||
|
}
|
||||||
33
api/edge/callbacks/internal/secrets/module.go
Normal file
33
api/edge/callbacks/internal/secrets/module.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/vault/kv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider resolves secrets by reference.
|
||||||
|
type Provider interface {
|
||||||
|
GetSecret(ctx context.Context, ref string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultOptions configure Vault KV secret resolution.
|
||||||
|
type VaultOptions struct {
|
||||||
|
Config kv.Config
|
||||||
|
DefaultField string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options configure secret lookup behavior.
|
||||||
|
type Options struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Static map[string]string
|
||||||
|
CacheTTL time.Duration
|
||||||
|
Vault VaultOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates secrets provider.
|
||||||
|
func New(opts Options) (Provider, error) {
|
||||||
|
return newProvider(opts)
|
||||||
|
}
|
||||||
224
api/edge/callbacks/internal/secrets/service.go
Normal file
224
api/edge/callbacks/internal/secrets/service.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/vault/kv"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultVaultField = "value"
|
||||||
|
vaultRefPrefix = "vault:"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cacheEntry struct {
|
||||||
|
value string
|
||||||
|
expiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
static map[string]string
|
||||||
|
ttl time.Duration
|
||||||
|
vault kv.Client
|
||||||
|
vaultEnabled bool
|
||||||
|
vaultDefField string
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
cache map[string]cacheEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProvider(opts Options) (Provider, error) {
|
||||||
|
logger := opts.Logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
static := map[string]string{}
|
||||||
|
for k, v := range opts.Static {
|
||||||
|
key := strings.TrimSpace(k)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
static[key] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
vaultField := strings.TrimSpace(opts.Vault.DefaultField)
|
||||||
|
if vaultField == "" {
|
||||||
|
vaultField = defaultVaultField
|
||||||
|
}
|
||||||
|
|
||||||
|
var vaultClient kv.Client
|
||||||
|
vaultEnabled := false
|
||||||
|
hasVaultConfig := strings.TrimSpace(opts.Vault.Config.Address) != "" ||
|
||||||
|
strings.TrimSpace(opts.Vault.Config.TokenEnv) != "" ||
|
||||||
|
strings.TrimSpace(opts.Vault.Config.MountPath) != ""
|
||||||
|
if hasVaultConfig {
|
||||||
|
client, err := kv.New(kv.Options{
|
||||||
|
Logger: logger.Named("vault"),
|
||||||
|
Config: opts.Vault.Config,
|
||||||
|
Component: "callbacks secrets",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vaultClient = client
|
||||||
|
vaultEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &provider{
|
||||||
|
logger: logger.Named("secrets"),
|
||||||
|
static: static,
|
||||||
|
ttl: opts.CacheTTL,
|
||||||
|
vault: vaultClient,
|
||||||
|
vaultEnabled: vaultEnabled,
|
||||||
|
vaultDefField: vaultField,
|
||||||
|
cache: map[string]cacheEntry{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) GetSecret(ctx context.Context, ref string) (string, error) {
|
||||||
|
key := strings.TrimSpace(ref)
|
||||||
|
if key == "" {
|
||||||
|
return "", merrors.InvalidArgument("secret reference is required", "secret_ref")
|
||||||
|
}
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := p.fromCache(key); ok {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := p.resolve(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return "", merrors.NoData("secret reference resolved to empty value")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.toCache(key, value)
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) resolve(ctx context.Context, key string) (string, error) {
|
||||||
|
if value, ok := p.static[key]; ok {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(key, "env:") {
|
||||||
|
envKey := strings.TrimSpace(strings.TrimPrefix(key, "env:"))
|
||||||
|
if envKey == "" {
|
||||||
|
return "", merrors.InvalidArgument("secret env reference is invalid", "secret_ref")
|
||||||
|
}
|
||||||
|
value := strings.TrimSpace(os.Getenv(envKey))
|
||||||
|
if value == "" {
|
||||||
|
return "", merrors.NoData("secret env variable not set: " + envKey)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(strings.ToLower(key), vaultRefPrefix) && !p.vaultEnabled {
|
||||||
|
return "", merrors.InvalidArgument("vault secret reference provided but vault is not configured", "secret_ref")
|
||||||
|
}
|
||||||
|
if p.vaultEnabled {
|
||||||
|
value, resolved, err := p.resolveVault(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resolved {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", merrors.NoData("secret reference not found: " + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) resolveVault(ctx context.Context, ref string) (string, bool, error) {
|
||||||
|
path, field, resolved, err := parseVaultRef(ref, p.vaultDefField)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
if !resolved {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := p.vault.GetString(ctx, path, field)
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Warn("Failed to resolve vault secret", zap.String("path", path), zap.String("field", field), zap.Error(err))
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
return value, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVaultRef(ref, defaultField string) (string, string, bool, error) {
|
||||||
|
raw := strings.TrimSpace(ref)
|
||||||
|
lowered := strings.ToLower(raw)
|
||||||
|
explicit := false
|
||||||
|
if strings.HasPrefix(lowered, vaultRefPrefix) {
|
||||||
|
explicit = true
|
||||||
|
raw = strings.TrimSpace(raw[len(vaultRefPrefix):])
|
||||||
|
}
|
||||||
|
|
||||||
|
if !explicit && !strings.Contains(raw, "/") && !strings.Contains(raw, "#") {
|
||||||
|
return "", "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
field := strings.TrimSpace(defaultField)
|
||||||
|
if field == "" {
|
||||||
|
field = defaultVaultField
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx := strings.Index(raw, "#"); idx >= 0 {
|
||||||
|
field = strings.TrimSpace(raw[idx+1:])
|
||||||
|
raw = strings.TrimSpace(raw[:idx])
|
||||||
|
if field == "" {
|
||||||
|
return "", "", false, merrors.InvalidArgument("vault secret field is required", "secret_ref")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.Trim(strings.TrimSpace(raw), "/")
|
||||||
|
if path == "" {
|
||||||
|
return "", "", false, merrors.InvalidArgument("vault secret path is required", "secret_ref")
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, field, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) fromCache(key string) (string, bool) {
|
||||||
|
if p.ttl <= 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
p.mu.RLock()
|
||||||
|
entry, ok := p.cache[key]
|
||||||
|
p.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if time.Now().After(entry.expiresAt) {
|
||||||
|
p.mu.Lock()
|
||||||
|
delete(p.cache, key)
|
||||||
|
p.mu.Unlock()
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return entry.value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) toCache(key, value string) {
|
||||||
|
if p.ttl <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.mu.Lock()
|
||||||
|
p.cache[key] = cacheEntry{
|
||||||
|
value: value,
|
||||||
|
expiresAt: time.Now().Add(p.ttl),
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
16
api/edge/callbacks/internal/security/module.go
Normal file
16
api/edge/callbacks/internal/security/module.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Config controls URL validation and SSRF checks.
|
||||||
|
type Config struct {
|
||||||
|
RequireHTTPS bool
|
||||||
|
AllowedHosts []string
|
||||||
|
AllowedPorts []int
|
||||||
|
DNSResolveTimeout int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator validates outbound callback URLs.
|
||||||
|
type Validator interface {
|
||||||
|
ValidateURL(ctx context.Context, target string) error
|
||||||
|
}
|
||||||
163
api/edge/callbacks/internal/security/service.go
Normal file
163
api/edge/callbacks/internal/security/service.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
requireHTTPS bool
|
||||||
|
allowedHosts map[string]struct{}
|
||||||
|
allowedPorts map[int]struct{}
|
||||||
|
dnsTimeout time.Duration
|
||||||
|
resolver *net.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates URL validator.
|
||||||
|
func New(cfg Config) Validator {
|
||||||
|
hosts := make(map[string]struct{}, len(cfg.AllowedHosts))
|
||||||
|
for _, host := range cfg.AllowedHosts {
|
||||||
|
h := strings.ToLower(strings.TrimSpace(host))
|
||||||
|
if h == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hosts[h] = struct{}{}
|
||||||
|
}
|
||||||
|
ports := make(map[int]struct{}, len(cfg.AllowedPorts))
|
||||||
|
for _, port := range cfg.AllowedPorts {
|
||||||
|
if port > 0 {
|
||||||
|
ports[port] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.Duration(cfg.DNSResolveTimeout) * time.Millisecond
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 2 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service{
|
||||||
|
requireHTTPS: cfg.RequireHTTPS,
|
||||||
|
allowedHosts: hosts,
|
||||||
|
allowedPorts: ports,
|
||||||
|
dnsTimeout: timeout,
|
||||||
|
resolver: net.DefaultResolver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ValidateURL(ctx context.Context, target string) error {
|
||||||
|
parsed, err := url.Parse(strings.TrimSpace(target))
|
||||||
|
if err != nil {
|
||||||
|
return merrors.InvalidArgumentWrap(err, "invalid callback URL", "url")
|
||||||
|
}
|
||||||
|
if parsed == nil || parsed.Host == "" {
|
||||||
|
return merrors.InvalidArgument("callback URL host is required", "url")
|
||||||
|
}
|
||||||
|
if parsed.User != nil {
|
||||||
|
return merrors.InvalidArgument("callback URL credentials are not allowed", "url")
|
||||||
|
}
|
||||||
|
if s.requireHTTPS && !strings.EqualFold(parsed.Scheme, "https") {
|
||||||
|
return merrors.InvalidArgument("callback URL must use HTTPS", "url")
|
||||||
|
}
|
||||||
|
|
||||||
|
host := strings.ToLower(strings.TrimSpace(parsed.Hostname()))
|
||||||
|
if host == "" {
|
||||||
|
return merrors.InvalidArgument("callback URL host is empty", "url")
|
||||||
|
}
|
||||||
|
if len(s.allowedHosts) > 0 {
|
||||||
|
if _, ok := s.allowedHosts[host]; !ok {
|
||||||
|
return merrors.InvalidArgument("callback host is not in allowlist", "url.host")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := resolvePort(parsed)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(s.allowedPorts) > 0 {
|
||||||
|
if _, ok := s.allowedPorts[port]; !ok {
|
||||||
|
return merrors.InvalidArgument("callback URL port is not allowed", "url.port")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr, addrErr := netip.ParseAddr(host); addrErr == nil {
|
||||||
|
if isBlocked(addr) {
|
||||||
|
return merrors.InvalidArgument("callback URL resolves to blocked IP range", "url")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lookupCtx := ctx
|
||||||
|
if lookupCtx == nil {
|
||||||
|
lookupCtx = context.Background()
|
||||||
|
}
|
||||||
|
lookupCtx, cancel := context.WithTimeout(lookupCtx, s.dnsTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ips, err := s.resolver.LookupIPAddr(lookupCtx, host)
|
||||||
|
if err != nil {
|
||||||
|
return merrors.InternalWrap(err, "failed to resolve callback host")
|
||||||
|
}
|
||||||
|
if len(ips) == 0 {
|
||||||
|
return merrors.InvalidArgument("callback host did not resolve", "url.host")
|
||||||
|
}
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.IP == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, ok := netip.AddrFromSlice(ip.IP)
|
||||||
|
if ok && isBlocked(addr) {
|
||||||
|
return merrors.InvalidArgument("callback URL resolves to blocked IP range", "url.host")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvePort(parsed *url.URL) (int, error) {
|
||||||
|
if parsed == nil {
|
||||||
|
return 0, merrors.InvalidArgument("callback URL is required", "url")
|
||||||
|
}
|
||||||
|
portStr := strings.TrimSpace(parsed.Port())
|
||||||
|
if portStr == "" {
|
||||||
|
if strings.EqualFold(parsed.Scheme, "https") {
|
||||||
|
return 443, nil
|
||||||
|
}
|
||||||
|
if strings.EqualFold(parsed.Scheme, "http") {
|
||||||
|
return 80, nil
|
||||||
|
}
|
||||||
|
return 0, merrors.InvalidArgument("callback URL scheme is not supported", "url.scheme")
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil || port <= 0 || port > 65535 {
|
||||||
|
return 0, merrors.InvalidArgument("callback URL port is invalid", "url.port")
|
||||||
|
}
|
||||||
|
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBlocked(ip netip.Addr) bool {
|
||||||
|
if !ip.IsValid() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ip.IsLoopback() || ip.IsPrivate() || ip.IsMulticast() || ip.IsUnspecified() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block common cloud metadata endpoint.
|
||||||
|
if ip.Is4() && ip.String() == "169.254.169.254" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
271
api/edge/callbacks/internal/server/internal/serverimp.go
Normal file
271
api/edge/callbacks/internal/server/internal/serverimp.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/config"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/delivery"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/events"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/ingest"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/ops"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/retry"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/secrets"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/security"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/signing"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/storage"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/subscriptions"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/health"
|
||||||
|
"github.com/tech/sendico/pkg/db"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/vault/kv"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultShutdownTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
type jetStreamProvider interface {
|
||||||
|
JetStream() nats.JetStreamContext
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
loader := config.New(i.logger)
|
||||||
|
cfg, err := loader.Load(i.file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.config = cfg
|
||||||
|
|
||||||
|
observer := ops.NewObserver()
|
||||||
|
metricsSrv, err := ops.NewHTTPServer(i.logger, ops.HTTPServerConfig{Address: cfg.Metrics.ListenAddress()})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.opServer = metricsSrv
|
||||||
|
i.opServer.SetStatus(health.SSStarting)
|
||||||
|
|
||||||
|
conn, err := db.ConnectMongo(i.logger.Named("mongo"), cfg.Database)
|
||||||
|
if err != nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.mongoConn = conn
|
||||||
|
|
||||||
|
repo, err := storage.New(i.logger, conn)
|
||||||
|
if err != nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver, err := subscriptions.New(subscriptions.Dependencies{EndpointRepo: repo.Endpoints()})
|
||||||
|
if err != nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
securityValidator := security.New(security.Config{
|
||||||
|
RequireHTTPS: cfg.Security.RequireHTTPS,
|
||||||
|
AllowedHosts: cfg.Security.AllowedHosts,
|
||||||
|
AllowedPorts: cfg.Security.AllowedPorts,
|
||||||
|
DNSResolveTimeout: int(cfg.Security.DNSResolveTimeoutMS() / time.Millisecond),
|
||||||
|
})
|
||||||
|
|
||||||
|
secretProvider, err := secrets.New(secrets.Options{
|
||||||
|
Logger: i.logger,
|
||||||
|
Static: cfg.Secrets.Static,
|
||||||
|
CacheTTL: cfg.Secrets.CacheTTL(),
|
||||||
|
Vault: secrets.VaultOptions{
|
||||||
|
Config: kv.Config{
|
||||||
|
Address: cfg.Secrets.Vault.Address,
|
||||||
|
TokenEnv: cfg.Secrets.Vault.TokenEnv,
|
||||||
|
Namespace: cfg.Secrets.Vault.Namespace,
|
||||||
|
MountPath: cfg.Secrets.Vault.MountPath,
|
||||||
|
},
|
||||||
|
DefaultField: cfg.Secrets.Vault.DefaultField,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
signer, err := signing.New(signing.Dependencies{Logger: i.logger, Provider: secretProvider})
|
||||||
|
if err != nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
retryPolicy := retry.New()
|
||||||
|
eventSvc := events.New(i.logger)
|
||||||
|
|
||||||
|
broker, err := msg.CreateMessagingBroker(i.logger.Named("messaging"), cfg.Messaging)
|
||||||
|
if err != nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.broker = broker
|
||||||
|
|
||||||
|
jsProvider, ok := broker.(jetStreamProvider)
|
||||||
|
if !ok || jsProvider.JetStream() == nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return merrors.Internal("callbacks: messaging broker does not provide JetStream")
|
||||||
|
}
|
||||||
|
|
||||||
|
ingestSvc, err := ingest.New(ingest.Dependencies{
|
||||||
|
Logger: i.logger,
|
||||||
|
JetStream: jsProvider.JetStream(),
|
||||||
|
Config: ingest.Config{
|
||||||
|
Stream: cfg.Ingest.Stream,
|
||||||
|
Subject: cfg.Ingest.Subject,
|
||||||
|
Durable: cfg.Ingest.Durable,
|
||||||
|
BatchSize: cfg.Ingest.BatchSize,
|
||||||
|
FetchTimeout: cfg.Ingest.FetchTimeout(),
|
||||||
|
IdleSleep: cfg.Ingest.IdleSleep(),
|
||||||
|
},
|
||||||
|
Events: eventSvc,
|
||||||
|
Resolver: resolver,
|
||||||
|
InboxRepo: repo.Inbox(),
|
||||||
|
TaskRepo: repo.Tasks(),
|
||||||
|
TaskDefaults: deliveryTaskDefaults(cfg),
|
||||||
|
Observer: observer,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.ingest = ingestSvc
|
||||||
|
|
||||||
|
deliverySvc, err := delivery.New(delivery.Dependencies{
|
||||||
|
Logger: i.logger,
|
||||||
|
Config: delivery.Config{
|
||||||
|
WorkerConcurrency: cfg.Delivery.WorkerConcurrency,
|
||||||
|
WorkerPoll: cfg.Delivery.WorkerPollInterval(),
|
||||||
|
LockTTL: cfg.Delivery.LockTTL(),
|
||||||
|
RequestTimeout: cfg.Delivery.RequestTimeout(),
|
||||||
|
JitterRatio: cfg.Delivery.JitterRatio,
|
||||||
|
},
|
||||||
|
Tasks: repo.Tasks(),
|
||||||
|
Retry: retryPolicy,
|
||||||
|
Security: securityValidator,
|
||||||
|
Signer: signer,
|
||||||
|
Observer: observer,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.delivery = deliverySvc
|
||||||
|
|
||||||
|
runCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
i.runCancel = cancel
|
||||||
|
i.ingest.Start(runCtx)
|
||||||
|
i.delivery.Start(runCtx)
|
||||||
|
i.opServer.SetStatus(health.SSRunning)
|
||||||
|
|
||||||
|
i.logger.Info("Callbacks service ready",
|
||||||
|
zap.String("subject", cfg.Ingest.Subject),
|
||||||
|
zap.String("stream", cfg.Ingest.Stream),
|
||||||
|
zap.Int("workers", cfg.Delivery.WorkerConcurrency),
|
||||||
|
)
|
||||||
|
|
||||||
|
<-i.stopCh
|
||||||
|
i.logger.Info("Callbacks service stop signal received")
|
||||||
|
i.shutdownRuntime(context.Background())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) Shutdown() {
|
||||||
|
i.signalStop()
|
||||||
|
if i.doneCh != nil {
|
||||||
|
<-i.doneCh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) shutdownRuntime(ctx context.Context) {
|
||||||
|
i.shutdown.Do(func() {
|
||||||
|
if i.opServer != nil {
|
||||||
|
i.opServer.SetStatus(health.SSTerminating)
|
||||||
|
}
|
||||||
|
if i.runCancel != nil {
|
||||||
|
i.runCancel()
|
||||||
|
}
|
||||||
|
if i.ingest != nil {
|
||||||
|
i.ingest.Stop()
|
||||||
|
}
|
||||||
|
if i.delivery != nil {
|
||||||
|
i.delivery.Stop()
|
||||||
|
}
|
||||||
|
if i.opServer != nil {
|
||||||
|
i.opServer.Close(ctx)
|
||||||
|
i.opServer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.mongoConn != nil {
|
||||||
|
timeout := i.shutdownTimeout()
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := i.mongoConn.Disconnect(shutdownCtx); err != nil {
|
||||||
|
i.logger.Warn("Failed to close MongoDB connection", zap.Error(err))
|
||||||
|
}
|
||||||
|
i.mongoConn = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Imp) shutdownTimeout() time.Duration {
|
||||||
|
if i.config != nil && i.config.Runtime != nil {
|
||||||
|
return i.config.Runtime.ShutdownTimeout()
|
||||||
|
}
|
||||||
|
return defaultShutdownTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
func deliveryTaskDefaults(cfg *config.Config) storage.TaskDefaults {
|
||||||
|
if cfg == nil {
|
||||||
|
return storage.TaskDefaults{}
|
||||||
|
}
|
||||||
|
return storage.TaskDefaults{
|
||||||
|
MaxAttempts: cfg.Delivery.MaxAttempts,
|
||||||
|
MinDelay: cfg.Delivery.MinDelay(),
|
||||||
|
MaxDelay: cfg.Delivery.MaxDelay(),
|
||||||
|
RequestTimeout: cfg.Delivery.RequestTimeout(),
|
||||||
|
}
|
||||||
|
}
|
||||||
37
api/edge/callbacks/internal/server/internal/types.go
Normal file
37
api/edge/callbacks/internal/server/internal/types.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package serverimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/config"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/delivery"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/ingest"
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/ops"
|
||||||
|
"github.com/tech/sendico/pkg/db"
|
||||||
|
mb "github.com/tech/sendico/pkg/messaging/broker"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Imp struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
file string
|
||||||
|
debug bool
|
||||||
|
|
||||||
|
config *config.Config
|
||||||
|
|
||||||
|
mongoConn *db.MongoConnection
|
||||||
|
broker mb.Broker
|
||||||
|
|
||||||
|
ingest ingest.Service
|
||||||
|
delivery delivery.Service
|
||||||
|
|
||||||
|
opServer ops.HTTPServer
|
||||||
|
|
||||||
|
runCancel context.CancelFunc
|
||||||
|
shutdown sync.Once
|
||||||
|
stopOnce sync.Once
|
||||||
|
doneOnce sync.Once
|
||||||
|
stopCh chan struct{}
|
||||||
|
doneCh chan struct{}
|
||||||
|
}
|
||||||
11
api/edge/callbacks/internal/server/server.go
Normal file
11
api/edge/callbacks/internal/server/server.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
serverimp "github.com/tech/sendico/edge/callbacks/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)
|
||||||
|
}
|
||||||
36
api/edge/callbacks/internal/signing/module.go
Normal file
36
api/edge/callbacks/internal/signing/module.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package signing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/secrets"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeNone = "none"
|
||||||
|
ModeHMACSHA256 = "hmac_sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignedPayload is what gets sent over HTTP.
|
||||||
|
type SignedPayload struct {
|
||||||
|
Body []byte
|
||||||
|
Headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer signs callback payloads.
|
||||||
|
type Signer interface {
|
||||||
|
Sign(ctx context.Context, mode, secretRef string, payload []byte, now time.Time) (*SignedPayload, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies configures signer service.
|
||||||
|
type Dependencies struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Provider secrets.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates signer service.
|
||||||
|
func New(deps Dependencies) (Signer, error) {
|
||||||
|
return newService(deps)
|
||||||
|
}
|
||||||
80
api/edge/callbacks/internal/signing/service.go
Normal file
80
api/edge/callbacks/internal/signing/service.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package signing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/secrets"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
provider secrets.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func newService(deps Dependencies) (Signer, error) {
|
||||||
|
if deps.Provider == nil {
|
||||||
|
return nil, merrors.InvalidArgument("signing: secrets provider is required", "provider")
|
||||||
|
}
|
||||||
|
logger := deps.Logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service{
|
||||||
|
logger: logger.Named("signing"),
|
||||||
|
provider: deps.Provider,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Sign(ctx context.Context, mode, secretRef string, payload []byte, now time.Time) (*SignedPayload, error) {
|
||||||
|
normalizedMode := strings.ToLower(strings.TrimSpace(mode))
|
||||||
|
if normalizedMode == "" {
|
||||||
|
normalizedMode = ModeNone
|
||||||
|
}
|
||||||
|
|
||||||
|
switch normalizedMode {
|
||||||
|
case ModeNone:
|
||||||
|
return &SignedPayload{
|
||||||
|
Body: append([]byte(nil), payload...),
|
||||||
|
Headers: map[string]string{},
|
||||||
|
}, nil
|
||||||
|
case ModeHMACSHA256:
|
||||||
|
if strings.TrimSpace(secretRef) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("signing: secret reference is required for hmac", "secret_ref")
|
||||||
|
}
|
||||||
|
secret, err := s.provider.GetSecret(ctx, secretRef)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to load signing secret", zap.String("secret_ref", secretRef), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := now.UTC().Format(time.RFC3339Nano)
|
||||||
|
mac := hmac.New(sha256.New, []byte(secret))
|
||||||
|
message := append([]byte(ts+"."), payload...)
|
||||||
|
if _, err := mac.Write(message); err != nil {
|
||||||
|
return nil, merrors.InternalWrap(err, "signing: failed to compute hmac")
|
||||||
|
}
|
||||||
|
signature := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
return &SignedPayload{
|
||||||
|
Body: append([]byte(nil), payload...),
|
||||||
|
Headers: map[string]string{
|
||||||
|
"X-Callback-Timestamp": ts,
|
||||||
|
"X-Callback-Signature": signature,
|
||||||
|
"X-Callback-Algorithm": "hmac-sha256",
|
||||||
|
"Content-Length": strconv.Itoa(len(payload)),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, merrors.InvalidArgument("signing: unsupported mode", "mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
99
api/edge/callbacks/internal/storage/module.go
Normal file
99
api/edge/callbacks/internal/storage/module.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskStatus tracks delivery task lifecycle.
|
||||||
|
type TaskStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskStatusPending TaskStatus = "PENDING"
|
||||||
|
TaskStatusRetry TaskStatus = "RETRY"
|
||||||
|
TaskStatusDelivered TaskStatus = "DELIVERED"
|
||||||
|
TaskStatusFailed TaskStatus = "FAILED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoint describes one target callback endpoint.
|
||||||
|
type Endpoint struct {
|
||||||
|
ID bson.ObjectID
|
||||||
|
ClientID string
|
||||||
|
URL string
|
||||||
|
SigningMode string
|
||||||
|
SecretRef string
|
||||||
|
Headers map[string]string
|
||||||
|
MaxAttempts int
|
||||||
|
MinDelay time.Duration
|
||||||
|
MaxDelay time.Duration
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task is one callback delivery job.
|
||||||
|
type Task struct {
|
||||||
|
ID bson.ObjectID
|
||||||
|
EventID string
|
||||||
|
EndpointID bson.ObjectID
|
||||||
|
EndpointURL string
|
||||||
|
SigningMode string
|
||||||
|
SecretRef string
|
||||||
|
Headers map[string]string
|
||||||
|
Payload []byte
|
||||||
|
Attempt int
|
||||||
|
MaxAttempts int
|
||||||
|
MinDelay time.Duration
|
||||||
|
MaxDelay time.Duration
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
Status TaskStatus
|
||||||
|
NextAttemptAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskDefaults are applied when creating tasks.
|
||||||
|
type TaskDefaults struct {
|
||||||
|
MaxAttempts int
|
||||||
|
MinDelay time.Duration
|
||||||
|
MaxDelay time.Duration
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options configures mongo collections.
|
||||||
|
type Options struct {
|
||||||
|
InboxCollection string
|
||||||
|
TasksCollection string
|
||||||
|
EndpointsCollection string
|
||||||
|
}
|
||||||
|
|
||||||
|
// InboxRepo controls event dedupe state.
|
||||||
|
type InboxRepo interface {
|
||||||
|
TryInsert(ctx context.Context, eventID, clientID, eventType string, at time.Time) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndpointRepo resolves endpoints for events.
|
||||||
|
type EndpointRepo interface {
|
||||||
|
FindActiveByClientAndType(ctx context.Context, clientID, eventType string) ([]Endpoint, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskRepo manages callback tasks.
|
||||||
|
type TaskRepo interface {
|
||||||
|
UpsertTasks(ctx context.Context, eventID string, endpoints []Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error
|
||||||
|
LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*Task, error)
|
||||||
|
MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error
|
||||||
|
MarkRetry(ctx context.Context, taskID bson.ObjectID, attempt int, nextAttemptAt time.Time, lastError string, httpCode int, at time.Time) error
|
||||||
|
MarkFailed(ctx context.Context, taskID bson.ObjectID, attempt int, lastError string, httpCode int, at time.Time) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository is the callbacks persistence contract.
|
||||||
|
type Repository interface {
|
||||||
|
Inbox() InboxRepo
|
||||||
|
Endpoints() EndpointRepo
|
||||||
|
Tasks() TaskRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a Mongo-backed callbacks repository.
|
||||||
|
func New(logger mlogger.Logger, conn *db.MongoConnection) (Repository, error) {
|
||||||
|
return newMongoRepository(logger, conn)
|
||||||
|
}
|
||||||
513
api/edge/callbacks/internal/storage/service.go
Normal file
513
api/edge/callbacks/internal/storage/service.go
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db"
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||||
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
mutil "github.com/tech/sendico/pkg/mutil/db"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
inboxCollection string = "inbox"
|
||||||
|
tasksCollection string = "tasks"
|
||||||
|
endpointsCollection string = "endpoints"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mongoRepository struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
|
||||||
|
inboxRepo repository.Repository
|
||||||
|
tasksRepo repository.Repository
|
||||||
|
endpointsRepo repository.Repository
|
||||||
|
|
||||||
|
inbox InboxRepo
|
||||||
|
endpoints EndpointRepo
|
||||||
|
tasks TaskRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
type inboxDoc struct {
|
||||||
|
storable.Base `bson:",inline"`
|
||||||
|
EventID string `bson:"event_id"`
|
||||||
|
ClientID string `bson:"client_id"`
|
||||||
|
EventType string `bson:"event_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *inboxDoc) Collection() string {
|
||||||
|
return inboxCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
type delayConfig struct {
|
||||||
|
MinDelayMS int `bson:"min_ms"`
|
||||||
|
MaxDelayMS int `bson:"max_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type deliveryPolicy struct {
|
||||||
|
delayConfig `bson:",inline"`
|
||||||
|
SigningMode string `bson:"signing_mode"`
|
||||||
|
SecretRef string `bson:"secret_ref"`
|
||||||
|
Headers map[string]string `bson:"headers"`
|
||||||
|
MaxAttempts int `bson:"max_attempts"`
|
||||||
|
RequestTimeoutMS int `bson:"request_timeout_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type endpointDoc struct {
|
||||||
|
storable.Base `bson:",inline"`
|
||||||
|
deliveryPolicy `bson:"retry_policy"`
|
||||||
|
ClientID string `bson:"client_id"`
|
||||||
|
Status string `bson:"status"`
|
||||||
|
URL string `bson:"url"`
|
||||||
|
EventTypes []string `bson:"event_types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *endpointDoc) Collection() string {
|
||||||
|
return endpointsCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskDoc struct {
|
||||||
|
storable.Base `bson:",inline"`
|
||||||
|
deliveryPolicy `bson:"retry_policy"`
|
||||||
|
EventID string `bson:"event_id"`
|
||||||
|
EndpointID bson.ObjectID `bson:"endpoint_id"`
|
||||||
|
EndpointURL string `bson:"endpoint_url"`
|
||||||
|
Payload []byte `bson:"payload"`
|
||||||
|
Status TaskStatus `bson:"status"`
|
||||||
|
Attempt int `bson:"attempt"`
|
||||||
|
LastError string `bson:"last_error,omitempty"`
|
||||||
|
LastHTTPCode int `bson:"last_http_code,omitempty"`
|
||||||
|
NextAttemptAt time.Time `bson:"next_attempt_at"`
|
||||||
|
LockedUntil *time.Time `bson:"locked_until,omitempty"`
|
||||||
|
WorkerID string `bson:"worker_id,omitempty"`
|
||||||
|
DeliveredAt *time.Time `bson:"delivered_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *taskDoc) Collection() string {
|
||||||
|
return tasksCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMongoRepository(logger mlogger.Logger, conn *db.MongoConnection) (Repository, error) {
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
if conn == nil {
|
||||||
|
return nil, merrors.InvalidArgument("callbacks storage: mongo connection is required", "conn")
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := &mongoRepository{
|
||||||
|
logger: logger.Named("storage"),
|
||||||
|
inboxRepo: repository.CreateMongoRepository(conn.Database(), inboxCollection),
|
||||||
|
tasksRepo: repository.CreateMongoRepository(conn.Database(), tasksCollection),
|
||||||
|
endpointsRepo: repository.CreateMongoRepository(conn.Database(), endpointsCollection),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.ensureIndexes(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.inbox = &inboxStore{logger: repo.logger.Named(repo.inboxRepo.Collection()), repo: repo.inboxRepo}
|
||||||
|
repo.endpoints = &endpointStore{logger: repo.logger.Named(repo.endpointsRepo.Collection()), repo: repo.endpointsRepo}
|
||||||
|
repo.tasks = &taskStore{logger: repo.logger.Named(repo.tasksRepo.Collection()), repo: repo.tasksRepo}
|
||||||
|
|
||||||
|
return repo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mongoRepository) Inbox() InboxRepo {
|
||||||
|
return m.inbox
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mongoRepository) Endpoints() EndpointRepo {
|
||||||
|
return m.endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mongoRepository) Tasks() TaskRepo {
|
||||||
|
return m.tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mongoRepository) ensureIndexes() error {
|
||||||
|
if err := m.inboxRepo.CreateIndex(&ri.Definition{
|
||||||
|
Name: "uq_event_id",
|
||||||
|
Unique: true,
|
||||||
|
Keys: []ri.Key{
|
||||||
|
{Field: "event_id", Sort: ri.Asc},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return merrors.InternalWrap(err, "callbacks storage: failed to create inbox indexes")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, def := range []*ri.Definition{
|
||||||
|
{
|
||||||
|
Name: "uq_event_endpoint",
|
||||||
|
Unique: true,
|
||||||
|
Keys: []ri.Key{
|
||||||
|
{Field: "event_id", Sort: ri.Asc},
|
||||||
|
{Field: "endpoint_id", Sort: ri.Asc},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "idx_dispatch_scan",
|
||||||
|
Keys: []ri.Key{
|
||||||
|
{Field: "status", Sort: ri.Asc},
|
||||||
|
{Field: "next_attempt_at", Sort: ri.Asc},
|
||||||
|
{Field: "locked_until", Sort: ri.Asc},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if err := m.tasksRepo.CreateIndex(def); err != nil {
|
||||||
|
return merrors.InternalWrap(err, "callbacks storage: failed to create tasks indexes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.endpointsRepo.CreateIndex(&ri.Definition{
|
||||||
|
Name: "idx_client_event",
|
||||||
|
Keys: []ri.Key{
|
||||||
|
{Field: "client_id", Sort: ri.Asc},
|
||||||
|
{Field: "status", Sort: ri.Asc},
|
||||||
|
{Field: "event_types", Sort: ri.Asc},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return merrors.InternalWrap(err, "callbacks storage: failed to create endpoint indexes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type inboxStore struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
repo repository.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *inboxStore) TryInsert(ctx context.Context, eventID, clientID, eventType string, at time.Time) (bool, error) {
|
||||||
|
doc := &inboxDoc{
|
||||||
|
EventID: strings.TrimSpace(eventID),
|
||||||
|
ClientID: strings.TrimSpace(clientID),
|
||||||
|
EventType: strings.TrimSpace(eventType),
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := repository.Filter("event_id", doc.EventID)
|
||||||
|
if err := r.repo.Insert(ctx, doc, filter); err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrDataConflict) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
r.logger.Warn("Failed to insert inbox dedupe marker", zap.String("event_id", eventID), zap.Error(err))
|
||||||
|
return false, merrors.InternalWrap(err, "callbacks inbox insert failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type endpointStore struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
repo repository.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *endpointStore) FindActiveByClientAndType(ctx context.Context, clientID, eventType string) ([]Endpoint, error) {
|
||||||
|
clientID = strings.TrimSpace(clientID)
|
||||||
|
eventType = strings.TrimSpace(eventType)
|
||||||
|
if clientID == "" {
|
||||||
|
return nil, merrors.InvalidArgument("client_id is required", "client_id")
|
||||||
|
}
|
||||||
|
if eventType == "" {
|
||||||
|
return nil, merrors.InvalidArgument("event type is required", "event_type")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := repository.Query().
|
||||||
|
Filter(repository.Field("client_id"), clientID).
|
||||||
|
In(repository.Field("status"), "active", "enabled")
|
||||||
|
|
||||||
|
out := make([]Endpoint, 0)
|
||||||
|
err := r.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
|
||||||
|
doc := &endpointDoc{}
|
||||||
|
if err := cur.Decode(doc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(doc.URL) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !supportsEventType(doc.EventTypes, eventType) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out = append(out, Endpoint{
|
||||||
|
ID: doc.ID,
|
||||||
|
ClientID: doc.ClientID,
|
||||||
|
URL: strings.TrimSpace(doc.URL),
|
||||||
|
SigningMode: strings.TrimSpace(doc.SigningMode),
|
||||||
|
SecretRef: strings.TrimSpace(doc.SecretRef),
|
||||||
|
Headers: cloneHeaders(doc.Headers),
|
||||||
|
MaxAttempts: doc.MaxAttempts,
|
||||||
|
MinDelay: time.Duration(doc.MinDelayMS) * time.Millisecond,
|
||||||
|
MaxDelay: time.Duration(doc.MaxDelayMS) * time.Millisecond,
|
||||||
|
RequestTimeout: time.Duration(doc.RequestTimeoutMS) * time.Millisecond,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||||
|
return nil, merrors.InternalWrap(err, "callbacks endpoint lookup failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportsEventType(eventTypes []string, eventType string) bool {
|
||||||
|
if len(eventTypes) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
eventType = strings.TrimSpace(eventType)
|
||||||
|
for _, t := range eventTypes {
|
||||||
|
current := strings.TrimSpace(t)
|
||||||
|
if current == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if current == "*" || current == eventType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskStore struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
repo repository.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints []Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error {
|
||||||
|
eventID = strings.TrimSpace(eventID)
|
||||||
|
if eventID == "" {
|
||||||
|
return merrors.InvalidArgument("event id is required", "event_id")
|
||||||
|
}
|
||||||
|
if len(endpoints) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
now := at.UTC()
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if endpoint.ID == bson.NilObjectID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
maxAttempts := endpoint.MaxAttempts
|
||||||
|
if maxAttempts <= 0 {
|
||||||
|
maxAttempts = defaults.MaxAttempts
|
||||||
|
}
|
||||||
|
if maxAttempts <= 0 {
|
||||||
|
maxAttempts = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
minDelay := endpoint.MinDelay
|
||||||
|
if minDelay <= 0 {
|
||||||
|
minDelay = defaults.MinDelay
|
||||||
|
}
|
||||||
|
if minDelay <= 0 {
|
||||||
|
minDelay = time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
maxDelay := endpoint.MaxDelay
|
||||||
|
if maxDelay <= 0 {
|
||||||
|
maxDelay = defaults.MaxDelay
|
||||||
|
}
|
||||||
|
if maxDelay < minDelay {
|
||||||
|
maxDelay = minDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
requestTimeout := endpoint.RequestTimeout
|
||||||
|
if requestTimeout <= 0 {
|
||||||
|
requestTimeout = defaults.RequestTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := &taskDoc{}
|
||||||
|
doc.EventID = eventID
|
||||||
|
doc.EndpointID = endpoint.ID
|
||||||
|
doc.EndpointURL = strings.TrimSpace(endpoint.URL)
|
||||||
|
doc.SigningMode = strings.TrimSpace(endpoint.SigningMode)
|
||||||
|
doc.SecretRef = strings.TrimSpace(endpoint.SecretRef)
|
||||||
|
doc.Headers = cloneHeaders(endpoint.Headers)
|
||||||
|
doc.Payload = append([]byte(nil), payload...)
|
||||||
|
doc.Status = TaskStatusPending
|
||||||
|
doc.Attempt = 0
|
||||||
|
doc.MaxAttempts = maxAttempts
|
||||||
|
doc.MinDelayMS = int(minDelay / time.Millisecond)
|
||||||
|
doc.MaxDelayMS = int(maxDelay / time.Millisecond)
|
||||||
|
doc.RequestTimeoutMS = int(requestTimeout / time.Millisecond)
|
||||||
|
doc.NextAttemptAt = now
|
||||||
|
|
||||||
|
filter := repository.Filter("event_id", eventID).And(repository.Filter("endpoint_id", endpoint.ID))
|
||||||
|
if err := r.repo.Insert(ctx, doc, filter); err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrDataConflict) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return merrors.InternalWrap(err, "callbacks task upsert failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*Task, error) {
|
||||||
|
workerID = strings.TrimSpace(workerID)
|
||||||
|
if workerID == "" {
|
||||||
|
return nil, merrors.InvalidArgument("worker id is required", "worker_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
now = now.UTC()
|
||||||
|
limit := int64(32)
|
||||||
|
lockFilter := repository.Query().Or(
|
||||||
|
repository.Query().Comparison(repository.Field("locked_until"), builder.Exists, false),
|
||||||
|
repository.Query().Filter(repository.Field("locked_until"), nil),
|
||||||
|
repository.Query().Comparison(repository.Field("locked_until"), builder.Lte, now),
|
||||||
|
)
|
||||||
|
|
||||||
|
query := repository.Query().
|
||||||
|
In(repository.Field("status"), string(TaskStatusPending), string(TaskStatusRetry)).
|
||||||
|
Comparison(repository.Field("next_attempt_at"), builder.Lte, now).
|
||||||
|
And(lockFilter).
|
||||||
|
Sort(repository.Field("next_attempt_at"), true).
|
||||||
|
Sort(repository.Field("created_at"), true).
|
||||||
|
Limit(&limit)
|
||||||
|
|
||||||
|
candidates, err := mutil.GetObjects[taskDoc](ctx, r.logger, query, nil, r.repo)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, merrors.InternalWrap(err, "callbacks task query failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
lockedUntil := now.Add(lockTTL)
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
patch := repository.Patch().
|
||||||
|
Set(repository.Field("locked_until"), lockedUntil).
|
||||||
|
Set(repository.Field("worker_id"), workerID)
|
||||||
|
|
||||||
|
conditional := repository.IDFilter(candidate.ID).And(
|
||||||
|
repository.Query().In(repository.Field("status"), string(TaskStatusPending), string(TaskStatusRetry)),
|
||||||
|
repository.Query().Comparison(repository.Field("next_attempt_at"), builder.Lte, now),
|
||||||
|
lockFilter,
|
||||||
|
)
|
||||||
|
|
||||||
|
updated, err := r.repo.PatchMany(ctx, conditional, patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.InternalWrap(err, "callbacks task lock update failed")
|
||||||
|
}
|
||||||
|
if updated == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
locked := &taskDoc{}
|
||||||
|
if err := r.repo.Get(ctx, candidate.ID, locked); err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, merrors.InternalWrap(err, "callbacks task lock reload failed")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(locked.WorkerID) != workerID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapTaskDoc(locked), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskStore) MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error {
|
||||||
|
_ = latency
|
||||||
|
if taskID == bson.NilObjectID {
|
||||||
|
return merrors.InvalidArgument("task id is required", "task_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := repository.Patch().
|
||||||
|
Set(repository.Field("status"), TaskStatusDelivered).
|
||||||
|
Set(repository.Field("last_http_code"), httpCode).
|
||||||
|
Set(repository.Field("delivered_at"), time.Now()).
|
||||||
|
Set(repository.Field("locked_until"), nil).
|
||||||
|
Set(repository.Field("worker_id"), "").
|
||||||
|
Set(repository.Field("last_error"), "")
|
||||||
|
|
||||||
|
if err := r.repo.Patch(ctx, taskID, patch); err != nil {
|
||||||
|
return merrors.InternalWrap(err, "callbacks task mark delivered failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskStore) MarkRetry(ctx context.Context, taskID bson.ObjectID, attempt int, nextAttemptAt time.Time, lastError string, httpCode int, at time.Time) error {
|
||||||
|
if taskID == bson.NilObjectID {
|
||||||
|
return merrors.InvalidArgument("task id is required", "task_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := repository.Patch().
|
||||||
|
Set(repository.Field("status"), TaskStatusRetry).
|
||||||
|
Set(repository.Field("attempt"), attempt).
|
||||||
|
Set(repository.Field("next_attempt_at"), nextAttemptAt.UTC()).
|
||||||
|
Set(repository.Field("last_error"), strings.TrimSpace(lastError)).
|
||||||
|
Set(repository.Field("last_http_code"), httpCode).
|
||||||
|
Set(repository.Field("locked_until"), nil).
|
||||||
|
Set(repository.Field("worker_id"), "")
|
||||||
|
|
||||||
|
if err := r.repo.Patch(ctx, taskID, patch); err != nil {
|
||||||
|
return merrors.InternalWrap(err, "callbacks task mark retry failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskStore) MarkFailed(ctx context.Context, taskID bson.ObjectID, attempt int, lastError string, httpCode int, at time.Time) error {
|
||||||
|
if taskID == bson.NilObjectID {
|
||||||
|
return merrors.InvalidArgument("task id is required", "task_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := repository.Patch().
|
||||||
|
Set(repository.Field("status"), TaskStatusFailed).
|
||||||
|
Set(repository.Field("attempt"), attempt).
|
||||||
|
Set(repository.Field("last_error"), strings.TrimSpace(lastError)).
|
||||||
|
Set(repository.Field("last_http_code"), httpCode).
|
||||||
|
Set(repository.Field("locked_until"), nil).
|
||||||
|
Set(repository.Field("worker_id"), "")
|
||||||
|
|
||||||
|
if err := r.repo.Patch(ctx, taskID, patch); err != nil {
|
||||||
|
return merrors.InternalWrap(err, "callbacks task mark failed failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapTaskDoc(doc *taskDoc) *Task {
|
||||||
|
if doc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Task{
|
||||||
|
ID: doc.ID,
|
||||||
|
EventID: doc.EventID,
|
||||||
|
EndpointID: doc.EndpointID,
|
||||||
|
EndpointURL: doc.EndpointURL,
|
||||||
|
SigningMode: doc.SigningMode,
|
||||||
|
SecretRef: doc.SecretRef,
|
||||||
|
Headers: cloneHeaders(doc.Headers),
|
||||||
|
Payload: append([]byte(nil), doc.Payload...),
|
||||||
|
Attempt: doc.Attempt,
|
||||||
|
MaxAttempts: doc.MaxAttempts,
|
||||||
|
MinDelay: time.Duration(doc.MinDelayMS) * time.Millisecond,
|
||||||
|
MaxDelay: time.Duration(doc.MaxDelayMS) * time.Millisecond,
|
||||||
|
RequestTimeout: time.Duration(doc.RequestTimeoutMS) * time.Millisecond,
|
||||||
|
Status: doc.Status,
|
||||||
|
NextAttemptAt: doc.NextAttemptAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneHeaders(in map[string]string) map[string]string {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
out := make(map[string]string, len(in))
|
||||||
|
for key, val := range in {
|
||||||
|
out[key] = val
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
17
api/edge/callbacks/internal/subscriptions/module.go
Normal file
17
api/edge/callbacks/internal/subscriptions/module.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package subscriptions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolver resolves active webhook endpoints for an event.
|
||||||
|
type Resolver interface {
|
||||||
|
Resolve(ctx context.Context, clientID, eventType string) ([]storage.Endpoint, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies defines subscriptions resolver dependencies.
|
||||||
|
type Dependencies struct {
|
||||||
|
EndpointRepo storage.EndpointRepo
|
||||||
|
}
|
||||||
38
api/edge/callbacks/internal/subscriptions/service.go
Normal file
38
api/edge/callbacks/internal/subscriptions/service.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package subscriptions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/storage"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo storage.EndpointRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates endpoint resolver service.
|
||||||
|
func New(deps Dependencies) (Resolver, error) {
|
||||||
|
if deps.EndpointRepo == nil {
|
||||||
|
return nil, merrors.InvalidArgument("subscriptions: endpoint repo is required", "endpointRepo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service{repo: deps.EndpointRepo}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Resolve(ctx context.Context, clientID, eventType string) ([]storage.Endpoint, error) {
|
||||||
|
if strings.TrimSpace(clientID) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("subscriptions: client id is required", "clientID")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(eventType) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("subscriptions: event type is required", "eventType")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := s.repo.FindActiveByClientAndType(ctx, clientID, eventType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints, nil
|
||||||
|
}
|
||||||
17
api/edge/callbacks/main.go
Normal file
17
api/edge/callbacks/main.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/edge/callbacks/internal/appversion"
|
||||||
|
si "github.com/tech/sendico/edge/callbacks/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("callbacks", appversion.Create(), factory)
|
||||||
|
}
|
||||||
@@ -7,9 +7,7 @@ replace github.com/tech/sendico/pkg => ../../pkg
|
|||||||
replace github.com/tech/sendico/gateway/common => ../common
|
replace github.com/tech/sendico/gateway/common => ../common
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1
|
|
||||||
github.com/ethereum/go-ethereum v1.17.0
|
github.com/ethereum/go-ethereum v1.17.0
|
||||||
github.com/hashicorp/vault/api v1.22.0
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/shopspring/decimal v1.4.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
@@ -38,6 +36,7 @@ require (
|
|||||||
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
|
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||||
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
|
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
@@ -55,6 +54,7 @@ require (
|
|||||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||||
|
github.com/hashicorp/vault/api v1.22.0 // indirect
|
||||||
github.com/holiman/uint256 v1.3.2 // indirect
|
github.com/holiman/uint256 v1.3.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
|||||||
@@ -2,18 +2,11 @@ package vault
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/tech/sendico/pkg/vault/managedkey"
|
||||||
"github.com/hashicorp/vault/api"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
@@ -23,20 +16,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Config describes how to connect to Vault for managed wallet keys.
|
// Config describes how to connect to Vault for managed wallet keys.
|
||||||
type Config struct {
|
type Config = managedkey.Config
|
||||||
Address string `mapstructure:"address"`
|
|
||||||
TokenEnv string `mapstructure:"token_env"`
|
|
||||||
Namespace string `mapstructure:"namespace"`
|
|
||||||
MountPath string `mapstructure:"mount_path"`
|
|
||||||
KeyPrefix string `mapstructure:"key_prefix"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manager implements the keymanager.Manager contract backed by HashiCorp Vault.
|
// Manager implements the keymanager.Manager contract backed by HashiCorp Vault.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
client *api.Client
|
keys managedkey.Service
|
||||||
store *api.KVv2
|
|
||||||
keyPrefix string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New constructs a Vault-backed key manager.
|
// New constructs a Vault-backed key manager.
|
||||||
@@ -44,227 +29,56 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
|
|||||||
if logger == nil {
|
if logger == nil {
|
||||||
return nil, merrors.InvalidArgument("vault key manager: logger is required")
|
return nil, merrors.InvalidArgument("vault key manager: logger is required")
|
||||||
}
|
}
|
||||||
address := strings.TrimSpace(cfg.Address)
|
keys, err := managedkey.New(managedkey.Options{
|
||||||
if address == "" {
|
Logger: logger,
|
||||||
logger.Error("Vault address missing")
|
Config: managedkey.Config(cfg),
|
||||||
return nil, merrors.InvalidArgument("vault key manager: address is required")
|
Component: "vault key manager",
|
||||||
}
|
DefaultKeyPrefix: "gateway/chain/wallets",
|
||||||
tokenEnv := strings.TrimSpace(cfg.TokenEnv)
|
})
|
||||||
if tokenEnv == "" {
|
|
||||||
logger.Error("Vault token env missing")
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: token_env is required")
|
|
||||||
}
|
|
||||||
token := strings.TrimSpace(os.Getenv(tokenEnv))
|
|
||||||
if token == "" {
|
|
||||||
logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
|
|
||||||
}
|
|
||||||
mountPath := strings.Trim(strings.TrimSpace(cfg.MountPath), "/")
|
|
||||||
if mountPath == "" {
|
|
||||||
logger.Error("Vault mount path missing")
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: mount_path is required")
|
|
||||||
}
|
|
||||||
keyPrefix := strings.Trim(strings.TrimSpace(cfg.KeyPrefix), "/")
|
|
||||||
if keyPrefix == "" {
|
|
||||||
keyPrefix = "gateway/chain/wallets"
|
|
||||||
}
|
|
||||||
|
|
||||||
clientCfg := api.DefaultConfig()
|
|
||||||
clientCfg.Address = address
|
|
||||||
|
|
||||||
client, err := api.NewClient(clientCfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to create vault client", zap.Error(err))
|
logger.Error("Failed to initialise vault key manager", zap.Error(err))
|
||||||
return nil, merrors.Internal("vault key manager: failed to create client: " + err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
client.SetToken(token)
|
|
||||||
if ns := strings.TrimSpace(cfg.Namespace); ns != "" {
|
|
||||||
client.SetNamespace(ns)
|
|
||||||
}
|
|
||||||
|
|
||||||
kv := client.KVv2(mountPath)
|
|
||||||
|
|
||||||
return &Manager{
|
return &Manager{
|
||||||
logger: logger.Named("vault"),
|
logger: logger.Named("vault"),
|
||||||
client: client,
|
keys: keys,
|
||||||
store: kv,
|
|
||||||
keyPrefix: keyPrefix,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
|
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
|
||||||
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network pmodel.ChainNetwork) (*keymanager.ManagedWalletKey, error) {
|
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network pmodel.ChainNetwork) (*keymanager.ManagedWalletKey, error) {
|
||||||
if strings.TrimSpace(walletRef) == "" {
|
|
||||||
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", string(network)))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: walletRef is required")
|
|
||||||
}
|
|
||||||
if network == pmodel.ChainNetworkUnspecified {
|
if network == pmodel.ChainNetworkUnspecified {
|
||||||
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
||||||
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
||||||
}
|
}
|
||||||
|
networkValue := strings.TrimSpace(string(network))
|
||||||
privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
|
if networkValue == "" {
|
||||||
if err != nil {
|
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
||||||
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", string(network)), zap.Error(err))
|
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
||||||
return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error())
|
|
||||||
}
|
}
|
||||||
privateKeyBytes := crypto.FromECDSA(privateKey)
|
|
||||||
publicKey := privateKey.PublicKey
|
|
||||||
publicKeyBytes := crypto.FromECDSAPub(&publicKey)
|
|
||||||
publicKeyHex := hex.EncodeToString(publicKeyBytes)
|
|
||||||
address := crypto.PubkeyToAddress(publicKey).Hex()
|
|
||||||
|
|
||||||
err = m.persistKey(ctx, walletRef, string(network), privateKeyBytes, publicKeyBytes, address)
|
created, err := m.keys.CreateManagedWalletKey(ctx, walletRef, networkValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", string(network)), zap.Error(err))
|
m.logger.Warn("Failed to create managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", networkValue), zap.Error(err))
|
||||||
zeroBytes(privateKeyBytes)
|
|
||||||
zeroBytes(publicKeyBytes)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
zeroBytes(privateKeyBytes)
|
|
||||||
zeroBytes(publicKeyBytes)
|
|
||||||
|
|
||||||
m.logger.Info("Managed wallet key created",
|
|
||||||
zap.String("wallet_ref", walletRef),
|
|
||||||
zap.String("network", string(network)),
|
|
||||||
zap.String("address", strings.ToLower(address)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return &keymanager.ManagedWalletKey{
|
return &keymanager.ManagedWalletKey{
|
||||||
KeyID: m.buildKeyID(string(network), walletRef),
|
KeyID: created.KeyID,
|
||||||
Address: strings.ToLower(address),
|
Address: created.Address,
|
||||||
PublicKey: publicKeyHex,
|
PublicKey: created.PublicKey,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) persistKey(ctx context.Context, walletRef, network string, privateKey, publicKey []byte, address string) error {
|
|
||||||
secretPath := m.buildKeyID(network, walletRef)
|
|
||||||
payload := map[string]interface{}{
|
|
||||||
"private_key": hex.EncodeToString(privateKey),
|
|
||||||
"public_key": hex.EncodeToString(publicKey),
|
|
||||||
"address": strings.ToLower(address),
|
|
||||||
"network": strings.ToLower(network),
|
|
||||||
}
|
|
||||||
if _, err := m.store.Put(ctx, secretPath, payload); err != nil {
|
|
||||||
return merrors.Internal("vault key manager: failed to write secret at " + secretPath + ": " + err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) buildKeyID(network, walletRef string) string {
|
|
||||||
net := strings.Trim(strings.ToLower(network), "/")
|
|
||||||
return path.Join(m.keyPrefix, net, walletRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignTransaction loads the key material from Vault and signs the transaction.
|
// SignTransaction loads the key material from Vault and signs the transaction.
|
||||||
func (m *Manager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
func (m *Manager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||||
if strings.TrimSpace(keyID) == "" {
|
signed, err := m.keys.SignEVMTransaction(ctx, keyID, tx, chainID)
|
||||||
m.logger.Warn("Signing failed: empty key id")
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: keyID is required")
|
|
||||||
}
|
|
||||||
if tx == nil {
|
|
||||||
m.logger.Warn("Signing failed: nil transaction", zap.String("key_id", keyID))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: transaction is nil")
|
|
||||||
}
|
|
||||||
if chainID == nil {
|
|
||||||
m.logger.Warn("Signing failed: nil chain id", zap.String("key_id", keyID))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: chainID is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
material, err := m.loadKey(ctx, keyID)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Failed to load key material", zap.String("key_id", keyID), zap.Error(err))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
keyBytes, err := hex.DecodeString(material.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Invalid key material", zap.String("key_id", keyID), zap.Error(err))
|
|
||||||
return nil, merrors.Internal("vault key manager: invalid key material: " + err.Error())
|
|
||||||
}
|
|
||||||
defer zeroBytes(keyBytes)
|
|
||||||
|
|
||||||
privateKey, err := crypto.ToECDSA(keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Failed to construct private key", zap.String("key_id", keyID), zap.Error(err))
|
|
||||||
return nil, merrors.Internal("vault key manager: failed to construct private key: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), privateKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
|
m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
|
||||||
return nil, merrors.Internal("vault key manager: failed to sign transaction: " + err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
m.logger.Info("Transaction signed with managed key",
|
|
||||||
zap.String("key_id", keyID),
|
|
||||||
zap.String("network", material.Network),
|
|
||||||
zap.String("tx_hash", signed.Hash().Hex()),
|
|
||||||
)
|
|
||||||
return signed, nil
|
return signed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyMaterial struct {
|
|
||||||
PrivateKey string
|
|
||||||
PublicKey string
|
|
||||||
Address string
|
|
||||||
Network string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) loadKey(ctx context.Context, keyID string) (*keyMaterial, error) {
|
|
||||||
secretPath := strings.Trim(strings.TrimPrefix(keyID, "/"), "/")
|
|
||||||
secret, err := m.store.Get(ctx, secretPath)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Failed to read secret", zap.String("path", secretPath), zap.Error(err))
|
|
||||||
return nil, merrors.Internal("vault key manager: failed to read secret at " + secretPath + ": " + err.Error())
|
|
||||||
}
|
|
||||||
if secret == nil || secret.Data == nil {
|
|
||||||
m.logger.Warn("Secret not found", zap.String("path", secretPath))
|
|
||||||
return nil, merrors.NoData("vault key manager: secret " + secretPath + " not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
getString := func(key string) (string, error) {
|
|
||||||
val, ok := secret.Data[key]
|
|
||||||
if !ok {
|
|
||||||
m.logger.Warn("Secret missing field", zap.String("path", secretPath), zap.String("field", key))
|
|
||||||
return "", merrors.Internal("vault key manager: secret " + secretPath + " missing " + key)
|
|
||||||
}
|
|
||||||
str, ok := val.(string)
|
|
||||||
if !ok || strings.TrimSpace(str) == "" {
|
|
||||||
m.logger.Warn("Secret field invalid", zap.String("path", secretPath), zap.String("field", key))
|
|
||||||
return "", merrors.Internal("vault key manager: secret " + secretPath + " invalid " + key)
|
|
||||||
}
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey, err := getString("private_key")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
publicKey, err := getString("public_key")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
address, err := getString("address")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
network, err := getString("network")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &keyMaterial{
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
PublicKey: publicKey,
|
|
||||||
Address: address,
|
|
||||||
Network: network,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func zeroBytes(data []byte) {
|
|
||||||
for i := range data {
|
|
||||||
data[i] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ keymanager.Manager = (*Manager)(nil)
|
var _ keymanager.Manager = (*Manager)(nil)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ require (
|
|||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1
|
||||||
github.com/ethereum/go-ethereum v1.17.0
|
github.com/ethereum/go-ethereum v1.17.0
|
||||||
github.com/fbsobreira/gotron-sdk v0.24.1
|
github.com/fbsobreira/gotron-sdk v0.24.1
|
||||||
github.com/hashicorp/vault/api v1.22.0
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/shengdoushi/base58 v1.0.0
|
github.com/shengdoushi/base58 v1.0.0
|
||||||
@@ -59,6 +58,7 @@ require (
|
|||||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||||
|
github.com/hashicorp/vault/api v1.22.0 // indirect
|
||||||
github.com/holiman/uint256 v1.3.2 // indirect
|
github.com/holiman/uint256 v1.3.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
|||||||
@@ -2,21 +2,16 @@ package vault
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
stdecdsa "crypto/ecdsa"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
troncore "github.com/fbsobreira/gotron-sdk/pkg/proto/core"
|
troncore "github.com/fbsobreira/gotron-sdk/pkg/proto/core"
|
||||||
"github.com/hashicorp/vault/api"
|
"github.com/tech/sendico/pkg/vault/managedkey"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
@@ -26,20 +21,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Config describes how to connect to Vault for managed wallet keys.
|
// Config describes how to connect to Vault for managed wallet keys.
|
||||||
type Config struct {
|
type Config = managedkey.Config
|
||||||
Address string `mapstructure:"address"`
|
|
||||||
TokenEnv string `mapstructure:"token_env"`
|
|
||||||
Namespace string `mapstructure:"namespace"`
|
|
||||||
MountPath string `mapstructure:"mount_path"`
|
|
||||||
KeyPrefix string `mapstructure:"key_prefix"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manager implements the keymanager.Manager contract backed by HashiCorp Vault.
|
// Manager implements the keymanager.Manager contract backed by HashiCorp Vault.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
client *api.Client
|
keys managedkey.Service
|
||||||
store *api.KVv2
|
|
||||||
keyPrefix string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New constructs a Vault-backed key manager.
|
// New constructs a Vault-backed key manager.
|
||||||
@@ -47,162 +34,45 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
|
|||||||
if logger == nil {
|
if logger == nil {
|
||||||
return nil, merrors.InvalidArgument("vault key manager: logger is required")
|
return nil, merrors.InvalidArgument("vault key manager: logger is required")
|
||||||
}
|
}
|
||||||
address := strings.TrimSpace(cfg.Address)
|
keys, err := managedkey.New(managedkey.Options{
|
||||||
if address == "" {
|
Logger: logger,
|
||||||
logger.Error("Vault address missing")
|
Config: managedkey.Config(cfg),
|
||||||
return nil, merrors.InvalidArgument("vault key manager: address is required")
|
Component: "vault key manager",
|
||||||
}
|
DefaultKeyPrefix: "gateway/tron/wallets",
|
||||||
tokenEnv := strings.TrimSpace(cfg.TokenEnv)
|
})
|
||||||
if tokenEnv == "" {
|
|
||||||
logger.Error("Vault token env missing")
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: token_env is required")
|
|
||||||
}
|
|
||||||
token := strings.TrimSpace(os.Getenv(tokenEnv))
|
|
||||||
if token == "" {
|
|
||||||
logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
|
|
||||||
}
|
|
||||||
mountPath := strings.Trim(strings.TrimSpace(cfg.MountPath), "/")
|
|
||||||
if mountPath == "" {
|
|
||||||
logger.Error("Vault mount path missing")
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: mount_path is required")
|
|
||||||
}
|
|
||||||
keyPrefix := strings.Trim(strings.TrimSpace(cfg.KeyPrefix), "/")
|
|
||||||
if keyPrefix == "" {
|
|
||||||
keyPrefix = "gateway/chain/wallets"
|
|
||||||
}
|
|
||||||
|
|
||||||
clientCfg := api.DefaultConfig()
|
|
||||||
clientCfg.Address = address
|
|
||||||
|
|
||||||
client, err := api.NewClient(clientCfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to create vault client", zap.Error(err))
|
logger.Error("Failed to initialise vault key manager", zap.Error(err))
|
||||||
return nil, merrors.Internal("vault key manager: failed to create client: " + err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
client.SetToken(token)
|
|
||||||
if ns := strings.TrimSpace(cfg.Namespace); ns != "" {
|
|
||||||
client.SetNamespace(ns)
|
|
||||||
}
|
|
||||||
|
|
||||||
kv := client.KVv2(mountPath)
|
|
||||||
|
|
||||||
return &Manager{
|
return &Manager{
|
||||||
logger: logger.Named("vault"),
|
logger: logger.Named("vault"),
|
||||||
client: client,
|
keys: keys,
|
||||||
store: kv,
|
|
||||||
keyPrefix: keyPrefix,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
|
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
|
||||||
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
|
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
|
||||||
if strings.TrimSpace(walletRef) == "" {
|
created, err := m.keys.CreateManagedWalletKey(ctx, walletRef, network)
|
||||||
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", network))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: walletRef is required")
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(network) == "" {
|
|
||||||
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey, err := stdecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
m.logger.Warn("Failed to create managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||||
return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error())
|
|
||||||
}
|
|
||||||
privateKeyBytes := crypto.FromECDSA(privateKey)
|
|
||||||
publicKey := privateKey.PublicKey
|
|
||||||
publicKeyBytes := crypto.FromECDSAPub(&publicKey)
|
|
||||||
publicKeyHex := hex.EncodeToString(publicKeyBytes)
|
|
||||||
address := crypto.PubkeyToAddress(publicKey).Hex()
|
|
||||||
|
|
||||||
err = m.persistKey(ctx, walletRef, network, privateKeyBytes, publicKeyBytes, address)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
|
||||||
zeroBytes(privateKeyBytes)
|
|
||||||
zeroBytes(publicKeyBytes)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
zeroBytes(privateKeyBytes)
|
|
||||||
zeroBytes(publicKeyBytes)
|
|
||||||
|
|
||||||
m.logger.Info("Managed wallet key created",
|
|
||||||
zap.String("wallet_ref", walletRef),
|
|
||||||
zap.String("network", network),
|
|
||||||
zap.String("address", strings.ToLower(address)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return &keymanager.ManagedWalletKey{
|
return &keymanager.ManagedWalletKey{
|
||||||
KeyID: m.buildKeyID(network, walletRef),
|
KeyID: created.KeyID,
|
||||||
Address: strings.ToLower(address),
|
Address: created.Address,
|
||||||
PublicKey: publicKeyHex,
|
PublicKey: created.PublicKey,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) persistKey(ctx context.Context, walletRef, network string, privateKey, publicKey []byte, address string) error {
|
|
||||||
secretPath := m.buildKeyID(network, walletRef)
|
|
||||||
payload := map[string]interface{}{
|
|
||||||
"private_key": hex.EncodeToString(privateKey),
|
|
||||||
"public_key": hex.EncodeToString(publicKey),
|
|
||||||
"address": strings.ToLower(address),
|
|
||||||
"network": strings.ToLower(network),
|
|
||||||
}
|
|
||||||
if _, err := m.store.Put(ctx, secretPath, payload); err != nil {
|
|
||||||
return merrors.Internal("vault key manager: failed to write secret at " + secretPath + ": " + err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) buildKeyID(network, walletRef string) string {
|
|
||||||
net := strings.Trim(strings.ToLower(network), "/")
|
|
||||||
return path.Join(m.keyPrefix, net, walletRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignTransaction loads the key material from Vault and signs the transaction.
|
// SignTransaction loads the key material from Vault and signs the transaction.
|
||||||
func (m *Manager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
func (m *Manager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||||
if strings.TrimSpace(keyID) == "" {
|
signed, err := m.keys.SignEVMTransaction(ctx, keyID, tx, chainID)
|
||||||
m.logger.Warn("Signing failed: empty key id")
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: keyID is required")
|
|
||||||
}
|
|
||||||
if tx == nil {
|
|
||||||
m.logger.Warn("Signing failed: nil transaction", zap.String("key_id", keyID))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: transaction is nil")
|
|
||||||
}
|
|
||||||
if chainID == nil {
|
|
||||||
m.logger.Warn("Signing failed: nil chain id", zap.String("key_id", keyID))
|
|
||||||
return nil, merrors.InvalidArgument("vault key manager: chainID is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
material, err := m.loadKey(ctx, keyID)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Failed to load key material", zap.String("key_id", keyID), zap.Error(err))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
keyBytes, err := hex.DecodeString(material.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Invalid key material", zap.String("key_id", keyID), zap.Error(err))
|
|
||||||
return nil, merrors.Internal("vault key manager: invalid key material: " + err.Error())
|
|
||||||
}
|
|
||||||
defer zeroBytes(keyBytes)
|
|
||||||
|
|
||||||
privateKey, err := crypto.ToECDSA(keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Failed to construct private key", zap.String("key_id", keyID), zap.Error(err))
|
|
||||||
return nil, merrors.Internal("vault key manager: failed to construct private key: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), privateKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
|
m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
|
||||||
return nil, merrors.Internal("vault key manager: failed to sign transaction: " + err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
m.logger.Info("Transaction signed with managed key",
|
|
||||||
zap.String("key_id", keyID),
|
|
||||||
zap.String("network", material.Network),
|
|
||||||
zap.String("tx_hash", signed.Hash().Hex()),
|
|
||||||
)
|
|
||||||
return signed, nil
|
return signed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +91,7 @@ func (m *Manager) SignTronTransaction(ctx context.Context, keyID string, tx *tro
|
|||||||
return nil, merrors.InvalidArgument("vault key manager: transaction raw_data is nil")
|
return nil, merrors.InvalidArgument("vault key manager: transaction raw_data is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
material, err := m.loadKey(ctx, keyID)
|
material, err := m.keys.LoadKeyMaterial(ctx, keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Failed to load key material for TRON signing", zap.String("key_id", keyID), zap.Error(err))
|
m.logger.Warn("Failed to load key material for TRON signing", zap.String("key_id", keyID), zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -274,64 +144,6 @@ func (m *Manager) SignTronTransaction(ctx context.Context, keyID string, tx *tro
|
|||||||
return tx, nil
|
return tx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyMaterial struct {
|
|
||||||
PrivateKey string
|
|
||||||
PublicKey string
|
|
||||||
Address string
|
|
||||||
Network string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) loadKey(ctx context.Context, keyID string) (*keyMaterial, error) {
|
|
||||||
secretPath := strings.Trim(strings.TrimPrefix(keyID, "/"), "/")
|
|
||||||
secret, err := m.store.Get(ctx, secretPath)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warn("Failed to read secret", zap.String("path", secretPath), zap.Error(err))
|
|
||||||
return nil, merrors.Internal("vault key manager: failed to read secret at " + secretPath + ": " + err.Error())
|
|
||||||
}
|
|
||||||
if secret == nil || secret.Data == nil {
|
|
||||||
m.logger.Warn("Secret not found", zap.String("path", secretPath))
|
|
||||||
return nil, merrors.NoData("vault key manager: secret " + secretPath + " not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
getString := func(key string) (string, error) {
|
|
||||||
val, ok := secret.Data[key]
|
|
||||||
if !ok {
|
|
||||||
m.logger.Warn("Secret missing field", zap.String("path", secretPath), zap.String("field", key))
|
|
||||||
return "", merrors.Internal("vault key manager: secret " + secretPath + " missing " + key)
|
|
||||||
}
|
|
||||||
str, ok := val.(string)
|
|
||||||
if !ok || strings.TrimSpace(str) == "" {
|
|
||||||
m.logger.Warn("Secret field invalid", zap.String("path", secretPath), zap.String("field", key))
|
|
||||||
return "", merrors.Internal("vault key manager: secret " + secretPath + " invalid " + key)
|
|
||||||
}
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey, err := getString("private_key")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
publicKey, err := getString("public_key")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
address, err := getString("address")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
network, err := getString("network")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &keyMaterial{
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
PublicKey: publicKey,
|
|
||||||
Address: address,
|
|
||||||
Network: network,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func zeroBytes(data []byte) {
|
func zeroBytes(data []byte) {
|
||||||
for i := range data {
|
for i := range data {
|
||||||
data[i] = 0
|
data[i] = 0
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func (i *Imp) Start() error {
|
|||||||
if broker != nil {
|
if broker != nil {
|
||||||
opts = append(opts, orchestrator.WithPaymentGatewayBroker(broker))
|
opts = append(opts, orchestrator.WithPaymentGatewayBroker(broker))
|
||||||
}
|
}
|
||||||
svc, err := orchestrator.NewService(logger, repo, opts...)
|
svc, err := orchestrator.NewService(logger, repo, producer, opts...)
|
||||||
i.service = svc
|
i.service = svc
|
||||||
return svc, err
|
return svc, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,6 +238,18 @@ func (s *svc) recordPaymentCreated(ctx context.Context, payment *agg.Payment, gr
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := s.statuses.Publish(ctx, paymentStatusPublishInput{
|
||||||
|
Payment: payment,
|
||||||
|
PreviousState: agg.StateUnspecified,
|
||||||
|
CurrentState: payment.State,
|
||||||
|
OccurredAt: s.nowUTC(),
|
||||||
|
Event: "created",
|
||||||
|
}); err != nil {
|
||||||
|
s.logger.Warn("Failed to publish created payment status update",
|
||||||
|
zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
for i := range payment.StepExecutions {
|
for i := range payment.StepExecutions {
|
||||||
if err := s.observer.RecordStep(ctx, oobs.RecordStepInput{
|
if err := s.observer.RecordStep(ctx, oobs.RecordStepInput{
|
||||||
PaymentRef: payment.PaymentRef,
|
PaymentRef: payment.PaymentRef,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ssched"
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ssched"
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||||
)
|
)
|
||||||
@@ -64,6 +65,7 @@ type Dependencies struct {
|
|||||||
Query pquery.Service
|
Query pquery.Service
|
||||||
Mapper prmap.Mapper
|
Mapper prmap.Mapper
|
||||||
Observer oobs.Observer
|
Observer oobs.Observer
|
||||||
|
Producer msg.Producer
|
||||||
|
|
||||||
RetryPolicy ssched.RetryPolicy
|
RetryPolicy ssched.RetryPolicy
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ func (s *svc) runRuntime(ctx context.Context, payment *agg.Payment) (*agg.Paymen
|
|||||||
func (s *svc) runTick(ctx context.Context, payment *agg.Payment, graph *xplan.Graph) (*agg.Payment, bool, bool, error) {
|
func (s *svc) runTick(ctx context.Context, payment *agg.Payment, graph *xplan.Graph) (*agg.Payment, bool, bool, error) {
|
||||||
logger := s.logger
|
logger := s.logger
|
||||||
expectedVersion := payment.Version
|
expectedVersion := payment.Version
|
||||||
|
previousAggregateState := payment.State
|
||||||
|
|
||||||
scheduled, err := s.scheduler.Schedule(ssched.Input{
|
scheduled, err := s.scheduler.Schedule(ssched.Input{
|
||||||
Steps: graph.Steps,
|
Steps: graph.Steps,
|
||||||
@@ -146,6 +147,22 @@ func (s *svc) runTick(ctx context.Context, payment *agg.Payment, graph *xplan.Gr
|
|||||||
zap.Uint64("version", payment.Version),
|
zap.Uint64("version", payment.Version),
|
||||||
zap.String("state", string(payment.State)),
|
zap.String("state", string(payment.State)),
|
||||||
)
|
)
|
||||||
|
if aggChanged && payment.State != previousAggregateState {
|
||||||
|
if err := s.statuses.Publish(ctx, paymentStatusPublishInput{
|
||||||
|
Payment: payment,
|
||||||
|
PreviousState: previousAggregateState,
|
||||||
|
CurrentState: payment.State,
|
||||||
|
OccurredAt: s.nowUTC(),
|
||||||
|
Event: "state_changed",
|
||||||
|
}); err != nil {
|
||||||
|
logger.Warn("Failed to publish payment status update",
|
||||||
|
zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
|
||||||
|
zap.String("from_state", string(previousAggregateState)),
|
||||||
|
zap.String("to_state", string(payment.State)),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
return payment, true, len(scheduled.Runnable) == 0, nil
|
return payment, true, len(scheduled.Runnable) == 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type svc struct {
|
|||||||
query pquery.Service
|
query pquery.Service
|
||||||
mapper prmap.Mapper
|
mapper prmap.Mapper
|
||||||
observer oobs.Observer
|
observer oobs.Observer
|
||||||
|
statuses paymentStatusPublisher
|
||||||
|
|
||||||
retryPolicy ssched.RetryPolicy
|
retryPolicy ssched.RetryPolicy
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
@@ -106,6 +107,7 @@ func newService(deps Dependencies) (Service, error) {
|
|||||||
query: query,
|
query: query,
|
||||||
mapper: firstMapper(deps.Mapper, logger),
|
mapper: firstMapper(deps.Mapper, logger),
|
||||||
observer: observer,
|
observer: observer,
|
||||||
|
statuses: newPaymentStatusPublisher(logger, deps.Producer),
|
||||||
|
|
||||||
retryPolicy: deps.RetryPolicy,
|
retryPolicy: deps.RetryPolicy,
|
||||||
now: deps.Now,
|
now: deps.Now,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package psvc
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
quotestorage "github.com/tech/sendico/payments/storage/quote"
|
quotestorage "github.com/tech/sendico/payments/storage/quote"
|
||||||
"github.com/tech/sendico/pkg/db/storable"
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
menv "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
pm "github.com/tech/sendico/pkg/model"
|
pm "github.com/tech/sendico/pkg/model"
|
||||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
@@ -76,6 +78,83 @@ func TestExecutePayment_EndToEndSyncSettled(t *testing.T) {
|
|||||||
assertTimelineHasEvent(t, timeline.Items, "settled")
|
assertTimelineHasEvent(t, timeline.Items, "settled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecutePayment_PublishesStatusUpdates(t *testing.T) {
|
||||||
|
env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
|
||||||
|
step := req.StepExecution
|
||||||
|
step.State = agg.StepStateCompleted
|
||||||
|
return &sexec.ExecuteOutput{StepExecution: step}, nil
|
||||||
|
})
|
||||||
|
env.quotes.Put(newExecutableQuote(env.orgID, "quote-status", "intent-status", buildLedgerRoute()))
|
||||||
|
|
||||||
|
resp, err := env.svc.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{
|
||||||
|
Meta: testMeta(env.orgID, "idem-status"),
|
||||||
|
QuotationRef: "quote-status",
|
||||||
|
ClientPaymentRef: "client-status",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExecutePayment returned error: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetPayment() == nil {
|
||||||
|
t.Fatal("expected payment in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
type publishedEnvelope struct {
|
||||||
|
EventID string `json:"event_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
type publishedData struct {
|
||||||
|
PaymentRef string `json:"payment_ref"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs := env.producer.Messages()
|
||||||
|
if len(msgs) == 0 {
|
||||||
|
t.Fatal("expected published status updates")
|
||||||
|
}
|
||||||
|
|
||||||
|
seenCreated := false
|
||||||
|
seenSettled := false
|
||||||
|
for i := range msgs {
|
||||||
|
if got, want := msgs[i].Subject, "payment_orchestrator_updated"; got != want {
|
||||||
|
t.Fatalf("subject mismatch at %d: got=%q want=%q", i, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var outer publishedEnvelope
|
||||||
|
if err := json.Unmarshal(msgs[i].Data, &outer); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal published envelope[%d]: %v", i, err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(outer.EventID) == "" {
|
||||||
|
t.Fatalf("expected non-empty event_id at %d", i)
|
||||||
|
}
|
||||||
|
if got, want := outer.Type, paymentStatusEventType; got != want {
|
||||||
|
t.Fatalf("event type mismatch at %d: got=%q want=%q", i, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var inner publishedData
|
||||||
|
if err := json.Unmarshal(outer.Data, &inner); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal published data[%d]: %v", i, err)
|
||||||
|
}
|
||||||
|
if got, want := inner.PaymentRef, resp.GetPayment().GetPaymentRef(); got != want {
|
||||||
|
t.Fatalf("payment_ref mismatch at %d: got=%q want=%q", i, got, want)
|
||||||
|
}
|
||||||
|
if inner.Event == "created" && inner.State == string(agg.StateCreated) {
|
||||||
|
seenCreated = true
|
||||||
|
}
|
||||||
|
if inner.State == string(agg.StateSettled) {
|
||||||
|
seenSettled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !seenCreated {
|
||||||
|
t.Fatal("expected created payment status update")
|
||||||
|
}
|
||||||
|
if !seenSettled {
|
||||||
|
t.Fatal("expected settled payment status update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExecutePayment_IdempotencyMismatch(t *testing.T) {
|
func TestExecutePayment_IdempotencyMismatch(t *testing.T) {
|
||||||
env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
|
env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
|
||||||
step := req.StepExecution
|
step := req.StepExecution
|
||||||
@@ -282,6 +361,7 @@ type testEnv struct {
|
|||||||
repo *memoryRepo
|
repo *memoryRepo
|
||||||
quotes *memoryQuoteStore
|
quotes *memoryQuoteStore
|
||||||
observer oobs.Observer
|
observer oobs.Observer
|
||||||
|
producer *capturingProducer
|
||||||
orgID bson.ObjectID
|
orgID bson.ObjectID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,11 +386,13 @@ func newTestEnv(t *testing.T, handler func(kind string, req sexec.StepRequest) (
|
|||||||
Guard: script,
|
Guard: script,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
producer := &capturingProducer{}
|
||||||
svc, err := New(Dependencies{
|
svc, err := New(Dependencies{
|
||||||
QuoteStore: quotes,
|
QuoteStore: quotes,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
Executors: registry,
|
Executors: registry,
|
||||||
Observer: observer,
|
Observer: observer,
|
||||||
|
Producer: producer,
|
||||||
RetryPolicy: ssched.RetryPolicy{MaxAttempts: 2},
|
RetryPolicy: ssched.RetryPolicy{MaxAttempts: 2},
|
||||||
MaxTicks: 20,
|
MaxTicks: 20,
|
||||||
})
|
})
|
||||||
@@ -322,10 +404,46 @@ func newTestEnv(t *testing.T, handler func(kind string, req sexec.StepRequest) (
|
|||||||
repo: repo,
|
repo: repo,
|
||||||
quotes: quotes,
|
quotes: quotes,
|
||||||
observer: observer,
|
observer: observer,
|
||||||
|
producer: producer,
|
||||||
orgID: bson.NewObjectID(),
|
orgID: bson.NewObjectID(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type capturedMessage struct {
|
||||||
|
Subject string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type capturingProducer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
items []capturedMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *capturingProducer) SendMessage(envelope menv.Envelope) error {
|
||||||
|
if envelope == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := envelope.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.mu.Lock()
|
||||||
|
p.items = append(p.items, capturedMessage{
|
||||||
|
Subject: envelope.GetSignature().ToString(),
|
||||||
|
Data: append([]byte(nil), data...),
|
||||||
|
})
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *capturingProducer) Messages() []capturedMessage {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
out := make([]capturedMessage, len(p.items))
|
||||||
|
copy(out, p.items)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
type scriptedExecutors struct {
|
type scriptedExecutors struct {
|
||||||
handler func(kind string, req sexec.StepRequest) (*sexec.ExecuteOutput, error)
|
handler func(kind string, req sexec.StepRequest) (*sexec.ExecuteOutput, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package psvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
|
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
nm "github.com/tech/sendico/pkg/model/notification"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
paymentStatusEventType = "payment.status.updated"
|
||||||
|
paymentStatusEventSender = "payments.orchestrator.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type paymentStatusPublisher interface {
|
||||||
|
Publish(ctx context.Context, in paymentStatusPublishInput) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type paymentStatusPublishInput struct {
|
||||||
|
Payment *agg.Payment
|
||||||
|
PreviousState agg.State
|
||||||
|
CurrentState agg.State
|
||||||
|
OccurredAt time.Time
|
||||||
|
Event string
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopPaymentStatusPublisher struct{}
|
||||||
|
|
||||||
|
func (noopPaymentStatusPublisher) Publish(_ context.Context, _ paymentStatusPublishInput) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type brokerPaymentStatusPublisher struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
producer msg.Producer
|
||||||
|
}
|
||||||
|
|
||||||
|
type callbackEventEnvelope struct {
|
||||||
|
EventID string `json:"event_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
OccurredAt time.Time `json:"occurred_at"`
|
||||||
|
PublishedAt time.Time `json:"published_at,omitempty"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type paymentStatusEventData struct {
|
||||||
|
OrganizationRef string `json:"organization_ref"`
|
||||||
|
PaymentRef string `json:"payment_ref"`
|
||||||
|
QuotationRef string `json:"quotation_ref"`
|
||||||
|
ClientPaymentRef string `json:"client_payment_ref,omitempty"`
|
||||||
|
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||||
|
State string `json:"state"`
|
||||||
|
PreviousState string `json:"previous_state,omitempty"`
|
||||||
|
Version uint64 `json:"version"`
|
||||||
|
IsTerminal bool `json:"is_terminal"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawEnvelope struct {
|
||||||
|
timestamp time.Time
|
||||||
|
messageID uuid.UUID
|
||||||
|
data []byte
|
||||||
|
sender string
|
||||||
|
signature model.NotificationEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPaymentStatusPublisher(logger mlogger.Logger, producer msg.Producer) paymentStatusPublisher {
|
||||||
|
if producer == nil {
|
||||||
|
return noopPaymentStatusPublisher{}
|
||||||
|
}
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
return &brokerPaymentStatusPublisher{
|
||||||
|
logger: logger.Named("status_publisher"),
|
||||||
|
producer: producer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *brokerPaymentStatusPublisher) Publish(_ context.Context, in paymentStatusPublishInput) error {
|
||||||
|
if in.Payment == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payment := in.Payment
|
||||||
|
|
||||||
|
paymentRef := strings.TrimSpace(payment.PaymentRef)
|
||||||
|
if paymentRef == "" || payment.OrganizationRef.IsZero() {
|
||||||
|
p.logger.Warn("Skipping payment status publish due to missing identifiers",
|
||||||
|
zap.String("payment_ref", paymentRef),
|
||||||
|
zap.String("organization_ref", payment.OrganizationRef.Hex()),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
occurredAt := in.OccurredAt.UTC()
|
||||||
|
if occurredAt.IsZero() {
|
||||||
|
occurredAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
eventName := strings.TrimSpace(in.Event)
|
||||||
|
if eventName == "" {
|
||||||
|
eventName = "state_changed"
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(paymentStatusEventData{
|
||||||
|
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||||
|
PaymentRef: paymentRef,
|
||||||
|
QuotationRef: strings.TrimSpace(payment.QuotationRef),
|
||||||
|
ClientPaymentRef: strings.TrimSpace(payment.ClientPaymentRef),
|
||||||
|
IdempotencyKey: strings.TrimSpace(payment.IdempotencyKey),
|
||||||
|
State: string(in.CurrentState),
|
||||||
|
PreviousState: normalizePreviousState(in.PreviousState, in.CurrentState),
|
||||||
|
Version: payment.Version,
|
||||||
|
IsTerminal: isTerminalState(in.CurrentState),
|
||||||
|
Event: eventName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return merrors.InternalWrap(err, "payment status publish: marshal body failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := json.Marshal(callbackEventEnvelope{
|
||||||
|
EventID: buildPaymentStatusEventID(paymentRef, payment.Version, in.CurrentState),
|
||||||
|
Type: paymentStatusEventType,
|
||||||
|
ClientID: payment.OrganizationRef.Hex(),
|
||||||
|
OccurredAt: occurredAt,
|
||||||
|
PublishedAt: time.Now().UTC(),
|
||||||
|
Data: body,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return merrors.InternalWrap(err, "payment status publish: marshal envelope failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := model.NewNotification(mservice.PaymentOrchestrator, nm.NAUpdated)
|
||||||
|
envelope := &rawEnvelope{
|
||||||
|
timestamp: occurredAt,
|
||||||
|
messageID: uuid.New(),
|
||||||
|
data: message,
|
||||||
|
sender: paymentStatusEventSender,
|
||||||
|
signature: signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.producer.SendMessage(envelope); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePreviousState(previous, current agg.State) string {
|
||||||
|
if previous == current || previous == agg.StateUnspecified {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTerminalState(state agg.State) bool {
|
||||||
|
switch state {
|
||||||
|
case agg.StateSettled, agg.StateNeedsAttention, agg.StateFailed:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPaymentStatusEventID(paymentRef string, version uint64, state agg.State) string {
|
||||||
|
return paymentRef + ":" + strconv.FormatUint(version, 10) + ":" + string(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rawEnvelope) Serialize() ([]byte, error) {
|
||||||
|
return append([]byte(nil), e.data...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rawEnvelope) GetTimeStamp() time.Time {
|
||||||
|
return e.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rawEnvelope) GetMessageId() uuid.UUID {
|
||||||
|
return e.messageID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rawEnvelope) GetData() []byte {
|
||||||
|
return append([]byte(nil), e.data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rawEnvelope) GetSender() string {
|
||||||
|
return e.sender
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rawEnvelope) GetSignature() model.NotificationEvent {
|
||||||
|
return e.signature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rawEnvelope) Wrap(data []byte) ([]byte, error) {
|
||||||
|
e.data = append([]byte(nil), data...)
|
||||||
|
return e.Serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ me.Envelope = (*rawEnvelope)(nil)
|
||||||
@@ -23,6 +23,7 @@ type Service struct {
|
|||||||
repo storage.Repository
|
repo storage.Repository
|
||||||
v2 psvc.Service
|
v2 psvc.Service
|
||||||
paymentRepo prepo.Repository
|
paymentRepo prepo.Repository
|
||||||
|
producer msg.Producer
|
||||||
|
|
||||||
ledgerClient ledgerclient.Client
|
ledgerClient ledgerclient.Client
|
||||||
mntxClient mntxclient.Client
|
mntxClient mntxclient.Client
|
||||||
@@ -35,7 +36,7 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewService constructs the v2 orchestrator service.
|
// NewService constructs the v2 orchestrator service.
|
||||||
func NewService(logger mlogger.Logger, repo storage.Repository, opts ...Option) (*Service, error) {
|
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) (*Service, error) {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = zap.NewNop()
|
logger = zap.NewNop()
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, opts ...Option)
|
|||||||
svc := &Service{
|
svc := &Service{
|
||||||
logger: logger.Named("service"),
|
logger: logger.Named("service"),
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
producer: producer,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -58,6 +60,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, opts ...Option)
|
|||||||
GatewayInvokeResolver: svc.gatewayInvokeResolver,
|
GatewayInvokeResolver: svc.gatewayInvokeResolver,
|
||||||
GatewayRegistry: svc.gatewayRegistry,
|
GatewayRegistry: svc.gatewayRegistry,
|
||||||
CardGatewayRoutes: svc.cardGatewayRoutes,
|
CardGatewayRoutes: svc.cardGatewayRoutes,
|
||||||
|
Producer: svc.producer,
|
||||||
})
|
})
|
||||||
svc.startExternalRuntime()
|
svc.startExternalRuntime()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/tech/sendico/payments/storage"
|
"github.com/tech/sendico/payments/storage"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||||
@@ -30,6 +31,7 @@ type v2RuntimeDeps struct {
|
|||||||
GatewayInvokeResolver GatewayInvokeResolver
|
GatewayInvokeResolver GatewayInvokeResolver
|
||||||
GatewayRegistry GatewayRegistry
|
GatewayRegistry GatewayRegistry
|
||||||
CardGatewayRoutes map[string]CardGatewayRoute
|
CardGatewayRoutes map[string]CardGatewayRoute
|
||||||
|
Producer msg.Producer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOrchestrationV2Service(logger mlogger.Logger, repo storage.Repository, runtimeDeps v2RuntimeDeps) (psvc.Service, prepo.Repository, error) {
|
func newOrchestrationV2Service(logger mlogger.Logger, repo storage.Repository, runtimeDeps v2RuntimeDeps) (psvc.Service, prepo.Repository, error) {
|
||||||
@@ -76,6 +78,7 @@ func newOrchestrationV2Service(logger mlogger.Logger, repo storage.Repository, r
|
|||||||
Query: query,
|
Query: query,
|
||||||
Observer: observer,
|
Observer: observer,
|
||||||
Executors: executors,
|
Executors: executors,
|
||||||
|
Producer: runtimeDeps.Producer,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Orchestration v2 disabled: service init failed", zap.Error(err))
|
logger.Error("Orchestration v2 disabled: service init failed", zap.Error(err))
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/casbin/casbin/v2 v2.135.0
|
github.com/casbin/casbin/v2 v2.135.0
|
||||||
github.com/casbin/mongodb-adapter/v4 v4.3.0
|
github.com/casbin/mongodb-adapter/v4 v4.3.0
|
||||||
|
github.com/ethereum/go-ethereum v1.17.0
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/hashicorp/vault/api v1.22.0
|
||||||
github.com/mattn/go-colorable v0.1.14
|
github.com/mattn/go-colorable v0.1.14
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/nats-io/nats.go v1.49.0
|
github.com/nats-io/nats.go v1.49.0
|
||||||
@@ -26,29 +28,47 @@ require (
|
|||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0 // indirect
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||||
github.com/casbin/govaluate v1.10.0 // indirect
|
github.com/casbin/govaluate v1.10.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/consensys/gnark-crypto v0.18.1 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/docker v27.3.1+incompatible // indirect
|
github.com/docker/docker v27.3.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||||
|
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||||
|
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||||
|
github.com/holiman/uint256 v1.3.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
@@ -67,10 +87,12 @@ require (
|
|||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.5 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.20.0 // indirect
|
github.com/prometheus/procfs v0.20.0 // indirect
|
||||||
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
@@ -82,7 +104,6 @@ require (
|
|||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
@@ -91,7 +112,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
|
||||||
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||||
|
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||||
|
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
@@ -20,19 +26,29 @@ github.com/casbin/mongodb-adapter/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0l
|
|||||||
github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE=
|
github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
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/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI=
|
||||||
|
github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
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/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 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
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 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
|
||||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
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/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 h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||||
@@ -41,10 +57,22 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
|||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
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 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
|
||||||
|
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
|
||||||
|
github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes=
|
||||||
|
github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
|
||||||
|
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
|
||||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
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/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
@@ -53,6 +81,10 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
|||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
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/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||||
|
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
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.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
@@ -66,18 +98,47 @@ 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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||||
|
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||||
|
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||||
|
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||||
|
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||||
|
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||||
|
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||||
|
github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
|
||||||
|
github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=
|
||||||
|
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
|
||||||
|
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||||
|
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
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/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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
@@ -86,6 +147,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
|||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||||
|
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
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/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 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
@@ -132,6 +197,10 @@ github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82
|
|||||||
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||||
|
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
|
||||||
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
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/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 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
@@ -146,6 +215,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
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/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw=
|
||||||
|
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||||
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
|
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 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 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
|
||||||
@@ -178,10 +249,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuH
|
|||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
||||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
@@ -190,8 +261,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
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/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 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
@@ -254,8 +325,8 @@ 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
@@ -269,8 +340,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||||
@@ -280,6 +351,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type Client struct {
|
|||||||
ClientID string `bson:"clientId"`
|
ClientID string `bson:"clientId"`
|
||||||
ClientName string `bson:"clientName"`
|
ClientName string `bson:"clientName"`
|
||||||
ClientSecret string `bson:"clientSecret,omitempty"`
|
ClientSecret string `bson:"clientSecret,omitempty"`
|
||||||
|
AllowedCIDRs []string `bson:"allowedCIDRs,omitempty"`
|
||||||
AllowedScopes []string `bson:"allowedScopes"`
|
AllowedScopes []string `bson:"allowedScopes"`
|
||||||
RedirectURIs []string `bson:"redirectURIs"`
|
RedirectURIs []string `bson:"redirectURIs"`
|
||||||
GrantTypes []string `bson:"grantTypes"`
|
GrantTypes []string `bson:"grantTypes"`
|
||||||
|
|||||||
34
api/pkg/vault/kv/module.go
Normal file
34
api/pkg/vault/kv/module.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config describes Vault KV v2 connection settings.
|
||||||
|
type Config struct {
|
||||||
|
Address string `mapstructure:"address" yaml:"address"`
|
||||||
|
TokenEnv string `mapstructure:"token_env" yaml:"token_env"`
|
||||||
|
Namespace string `mapstructure:"namespace" yaml:"namespace"`
|
||||||
|
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client defines KV operations used by services.
|
||||||
|
type Client interface {
|
||||||
|
Put(ctx context.Context, secretPath string, payload map[string]interface{}) error
|
||||||
|
Get(ctx context.Context, secretPath string) (map[string]interface{}, error)
|
||||||
|
GetString(ctx context.Context, secretPath, field string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options configure KV client creation.
|
||||||
|
type Options struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Config Config
|
||||||
|
Component string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a Vault KV v2 client.
|
||||||
|
func New(opts Options) (Client, error) {
|
||||||
|
return newService(opts)
|
||||||
|
}
|
||||||
151
api/pkg/vault/kv/service.go
Normal file
151
api/pkg/vault/kv/service.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultComponent = "vault kv"
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
component string
|
||||||
|
store *api.KVv2
|
||||||
|
}
|
||||||
|
|
||||||
|
func newService(opts Options) (Client, error) {
|
||||||
|
logger := opts.Logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
component := strings.TrimSpace(opts.Component)
|
||||||
|
if component == "" {
|
||||||
|
component = defaultComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
address := strings.TrimSpace(opts.Config.Address)
|
||||||
|
if address == "" {
|
||||||
|
logger.Error("Vault address missing")
|
||||||
|
return nil, merrors.InvalidArgument(component + ": address is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenEnv := strings.TrimSpace(opts.Config.TokenEnv)
|
||||||
|
if tokenEnv == "" {
|
||||||
|
logger.Error("Vault token env missing")
|
||||||
|
return nil, merrors.InvalidArgument(component + ": token_env is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimSpace(os.Getenv(tokenEnv))
|
||||||
|
if token == "" {
|
||||||
|
logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
|
||||||
|
return nil, merrors.InvalidArgument(component + ": token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
|
||||||
|
}
|
||||||
|
|
||||||
|
mountPath := strings.Trim(strings.TrimSpace(opts.Config.MountPath), "/")
|
||||||
|
if mountPath == "" {
|
||||||
|
logger.Error("Vault mount path missing")
|
||||||
|
return nil, merrors.InvalidArgument(component + ": mount_path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCfg := api.DefaultConfig()
|
||||||
|
clientCfg.Address = address
|
||||||
|
|
||||||
|
client, err := api.NewClient(clientCfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to create vault client", zap.Error(err))
|
||||||
|
return nil, merrors.Internal(component + ": failed to create client: " + err.Error())
|
||||||
|
}
|
||||||
|
client.SetToken(token)
|
||||||
|
|
||||||
|
if ns := strings.TrimSpace(opts.Config.Namespace); ns != "" {
|
||||||
|
client.SetNamespace(ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service{
|
||||||
|
logger: logger.Named("vault"),
|
||||||
|
component: component,
|
||||||
|
store: client.KVv2(mountPath),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Put(ctx context.Context, secretPath string, payload map[string]interface{}) error {
|
||||||
|
if payload == nil {
|
||||||
|
return merrors.InvalidArgument(s.component+": payload is required", "payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedPath, err := normalizePath(secretPath)
|
||||||
|
if err != nil {
|
||||||
|
return merrors.InvalidArgumentWrap(err, s.component+": secret path is invalid", "secret_path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.store.Put(ctx, normalizedPath, payload); err != nil {
|
||||||
|
s.logger.Warn("Failed to write secret", zap.String("path", normalizedPath), zap.Error(err))
|
||||||
|
return merrors.Internal(s.component + ": failed to write secret at " + normalizedPath + ": " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Get(ctx context.Context, secretPath string) (map[string]interface{}, error) {
|
||||||
|
normalizedPath, err := normalizePath(secretPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.InvalidArgumentWrap(err, s.component+": secret path is invalid", "secret_path")
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := s.store.Get(ctx, normalizedPath)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to read secret", zap.String("path", normalizedPath), zap.Error(err))
|
||||||
|
return nil, merrors.Internal(s.component + ": failed to read secret at " + normalizedPath + ": " + err.Error())
|
||||||
|
}
|
||||||
|
if secret == nil || secret.Data == nil {
|
||||||
|
return nil, merrors.NoData(s.component + ": secret " + normalizedPath + " not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]interface{}, len(secret.Data))
|
||||||
|
for k, v := range secret.Data {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetString(ctx context.Context, secretPath, field string) (string, error) {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if field == "" {
|
||||||
|
return "", merrors.InvalidArgument(s.component+": field is required", "field")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := s.Get(ctx, secretPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := data[field]
|
||||||
|
if !ok {
|
||||||
|
return "", merrors.Internal(s.component + ": secret " + strings.Trim(strings.TrimPrefix(secretPath, "/"), "/") + " missing " + field)
|
||||||
|
}
|
||||||
|
|
||||||
|
str, ok := val.(string)
|
||||||
|
if !ok || strings.TrimSpace(str) == "" {
|
||||||
|
return "", merrors.Internal(s.component + ": secret " + strings.Trim(strings.TrimPrefix(secretPath, "/"), "/") + " invalid " + field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePath(secretPath string) (string, error) {
|
||||||
|
normalizedPath := strings.Trim(strings.TrimPrefix(strings.TrimSpace(secretPath), "/"), "/")
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return "", merrors.InvalidArgument("secret path is required", "secret_path")
|
||||||
|
}
|
||||||
|
return normalizedPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Client = (*service)(nil)
|
||||||
54
api/pkg/vault/managedkey/module.go
Normal file
54
api/pkg/vault/managedkey/module.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package managedkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config describes how to connect to Vault for managed wallet keys.
|
||||||
|
type Config struct {
|
||||||
|
Address string `mapstructure:"address" yaml:"address"`
|
||||||
|
TokenEnv string `mapstructure:"token_env" yaml:"token_env"`
|
||||||
|
Namespace string `mapstructure:"namespace" yaml:"namespace"`
|
||||||
|
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
||||||
|
KeyPrefix string `mapstructure:"key_prefix" yaml:"key_prefix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagedWalletKey captures metadata returned after key provisioning.
|
||||||
|
type ManagedWalletKey struct {
|
||||||
|
KeyID string
|
||||||
|
Address string
|
||||||
|
PublicKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material contains key material loaded from Vault.
|
||||||
|
type Material struct {
|
||||||
|
PrivateKey string
|
||||||
|
PublicKey string
|
||||||
|
Address string
|
||||||
|
Network string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service defines managed key operations shared by gateways.
|
||||||
|
type Service interface {
|
||||||
|
CreateManagedWalletKey(ctx context.Context, walletRef, network string) (*ManagedWalletKey, error)
|
||||||
|
SignEVMTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
|
||||||
|
LoadKeyMaterial(ctx context.Context, keyID string) (*Material, error)
|
||||||
|
BuildKeyID(network, walletRef string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options configure managed key service creation.
|
||||||
|
type Options struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Config Config
|
||||||
|
Component string
|
||||||
|
DefaultKeyPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a managed wallet key service backed by Vault KV.
|
||||||
|
func New(opts Options) (Service, error) {
|
||||||
|
return newService(opts)
|
||||||
|
}
|
||||||
218
api/pkg/vault/managedkey/service.go
Normal file
218
api/pkg/vault/managedkey/service.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package managedkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"math/big"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/vault/kv"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultComponent = "vault key manager"
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
component string
|
||||||
|
store kv.Client
|
||||||
|
keyPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newService(opts Options) (Service, error) {
|
||||||
|
logger := opts.Logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
component := strings.TrimSpace(opts.Component)
|
||||||
|
if component == "" {
|
||||||
|
component = defaultComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := kv.New(kv.Options{
|
||||||
|
Logger: logger,
|
||||||
|
Config: kv.Config{
|
||||||
|
Address: opts.Config.Address,
|
||||||
|
TokenEnv: opts.Config.TokenEnv,
|
||||||
|
Namespace: opts.Config.Namespace,
|
||||||
|
MountPath: opts.Config.MountPath,
|
||||||
|
},
|
||||||
|
Component: component,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPrefix := strings.Trim(strings.TrimSpace(opts.Config.KeyPrefix), "/")
|
||||||
|
if keyPrefix == "" {
|
||||||
|
keyPrefix = strings.Trim(strings.TrimSpace(opts.DefaultKeyPrefix), "/")
|
||||||
|
}
|
||||||
|
if keyPrefix == "" {
|
||||||
|
keyPrefix = "wallets"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service{
|
||||||
|
logger: logger.Named("vault"),
|
||||||
|
component: component,
|
||||||
|
store: store,
|
||||||
|
keyPrefix: keyPrefix,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) CreateManagedWalletKey(ctx context.Context, walletRef, network string) (*ManagedWalletKey, error) {
|
||||||
|
walletRef = strings.TrimSpace(walletRef)
|
||||||
|
network = strings.TrimSpace(network)
|
||||||
|
if walletRef == "" {
|
||||||
|
s.logger.Warn("WalletRef missing for managed key creation", zap.String("network", network))
|
||||||
|
return nil, merrors.InvalidArgument(s.component+": walletRef is required", "wallet_ref")
|
||||||
|
}
|
||||||
|
if network == "" {
|
||||||
|
s.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
||||||
|
return nil, merrors.InvalidArgument(s.component+": network is required", "network")
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||||
|
return nil, merrors.Internal(s.component + ": failed to generate key: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyBytes := crypto.FromECDSA(privateKey)
|
||||||
|
publicKey := privateKey.PublicKey
|
||||||
|
publicKeyBytes := crypto.FromECDSAPub(&publicKey)
|
||||||
|
publicKeyHex := hex.EncodeToString(publicKeyBytes)
|
||||||
|
address := strings.ToLower(crypto.PubkeyToAddress(publicKey).Hex())
|
||||||
|
keyID := s.BuildKeyID(network, walletRef)
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"private_key": hex.EncodeToString(privateKeyBytes),
|
||||||
|
"public_key": hex.EncodeToString(publicKeyBytes),
|
||||||
|
"address": address,
|
||||||
|
"network": strings.ToLower(network),
|
||||||
|
}
|
||||||
|
if err := s.store.Put(ctx, keyID, payload); err != nil {
|
||||||
|
s.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||||
|
zeroBytes(privateKeyBytes)
|
||||||
|
zeroBytes(publicKeyBytes)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroBytes(privateKeyBytes)
|
||||||
|
zeroBytes(publicKeyBytes)
|
||||||
|
|
||||||
|
s.logger.Info("Managed wallet key created", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.String("address", address))
|
||||||
|
|
||||||
|
return &ManagedWalletKey{
|
||||||
|
KeyID: keyID,
|
||||||
|
Address: address,
|
||||||
|
PublicKey: publicKeyHex,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) SignEVMTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||||
|
if strings.TrimSpace(keyID) == "" {
|
||||||
|
s.logger.Warn("Signing failed: empty key id")
|
||||||
|
return nil, merrors.InvalidArgument(s.component+": keyID is required", "key_id")
|
||||||
|
}
|
||||||
|
if tx == nil {
|
||||||
|
s.logger.Warn("Signing failed: nil transaction", zap.String("key_id", keyID))
|
||||||
|
return nil, merrors.InvalidArgument(s.component+": transaction is nil", "transaction")
|
||||||
|
}
|
||||||
|
if chainID == nil {
|
||||||
|
s.logger.Warn("Signing failed: nil chain id", zap.String("key_id", keyID))
|
||||||
|
return nil, merrors.InvalidArgument(s.component+": chainID is nil", "chain_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
material, err := s.LoadKeyMaterial(ctx, keyID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to load key material", zap.String("key_id", keyID), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes, err := hex.DecodeString(material.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Invalid key material", zap.String("key_id", keyID), zap.Error(err))
|
||||||
|
return nil, merrors.Internal(s.component + ": invalid key material: " + err.Error())
|
||||||
|
}
|
||||||
|
defer zeroBytes(keyBytes)
|
||||||
|
|
||||||
|
privateKey, err := crypto.ToECDSA(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to construct private key", zap.String("key_id", keyID), zap.Error(err))
|
||||||
|
return nil, merrors.Internal(s.component + ": failed to construct private key: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), privateKey)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
|
||||||
|
return nil, merrors.Internal(s.component + ": failed to sign transaction: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Transaction signed with managed key", zap.String("key_id", keyID), zap.String("network", material.Network), zap.String("tx_hash", signed.Hash().Hex()))
|
||||||
|
|
||||||
|
return signed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) LoadKeyMaterial(ctx context.Context, keyID string) (*Material, error) {
|
||||||
|
data, err := s.store.Get(ctx, keyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secretPath := strings.Trim(strings.TrimPrefix(strings.TrimSpace(keyID), "/"), "/")
|
||||||
|
privateKey, err := fieldAsString(data, "private_key", secretPath, s.component)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
publicKey, err := fieldAsString(data, "public_key", secretPath, s.component)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
address, err := fieldAsString(data, "address", secretPath, s.component)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
network, err := fieldAsString(data, "network", secretPath, s.component)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Material{
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
PublicKey: publicKey,
|
||||||
|
Address: address,
|
||||||
|
Network: network,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) BuildKeyID(network, walletRef string) string {
|
||||||
|
net := strings.Trim(strings.ToLower(strings.TrimSpace(network)), "/")
|
||||||
|
ref := strings.Trim(strings.TrimSpace(walletRef), "/")
|
||||||
|
return path.Join(s.keyPrefix, net, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldAsString(data map[string]interface{}, field, secretPath, component string) (string, error) {
|
||||||
|
value, ok := data[field]
|
||||||
|
if !ok {
|
||||||
|
return "", merrors.Internal(component + ": secret " + secretPath + " missing " + field)
|
||||||
|
}
|
||||||
|
str, ok := value.(string)
|
||||||
|
if !ok || strings.TrimSpace(str) == "" {
|
||||||
|
return "", merrors.Internal(component + ": secret " + secretPath + " invalid " + field)
|
||||||
|
}
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func zeroBytes(data []byte) {
|
||||||
|
for i := range data {
|
||||||
|
data[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Service = (*service)(nil)
|
||||||
@@ -12,7 +12,7 @@ Docker Compose + Makefile build system for local development.
|
|||||||
**Services:**
|
**Services:**
|
||||||
- Discovery, Ledger, Billing Fees, Billing Documents, FX Oracle, Payments Orchestrator
|
- Discovery, Ledger, Billing Fees, Billing Documents, FX Oracle, Payments Orchestrator
|
||||||
- Chain Gateway, MNTX Gateway, TGSettle Gateway
|
- Chain Gateway, MNTX Gateway, TGSettle Gateway
|
||||||
- FX Ingestor, Notification, BFF (Server), Frontend
|
- FX Ingestor, Notification, BFF (Server), Callbacks, Frontend
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -60,10 +60,14 @@ make status # Check service status
|
|||||||
Examples:
|
Examples:
|
||||||
- Blockchain private keys (Chain Gateway)
|
- Blockchain private keys (Chain Gateway)
|
||||||
- External API keys (MNTX, TGSettle)
|
- External API keys (MNTX, TGSettle)
|
||||||
|
- Webhook signing secrets (Callbacks)
|
||||||
- Production-like secrets
|
- Production-like secrets
|
||||||
|
|
||||||
Infrastructure (MongoDB, NATS) uses plain `.env.dev` credentials.
|
Infrastructure (MongoDB, NATS) uses plain `.env.dev` credentials.
|
||||||
|
|
||||||
|
Callbacks, Chain, and TRON run Vault Agent sidecars with AppRole.
|
||||||
|
Set the corresponding `*_VAULT_ROLE_ID` and `*_VAULT_SECRET_ID` values in `.env.dev`.
|
||||||
|
|
||||||
## Network
|
## Network
|
||||||
|
|
||||||
All services on `sendico-dev` network. Vault also on `cicd` network to connect to infra Vault if needed.
|
All services on `sendico-dev` network. Vault also on `cicd` network to connect to infra Vault if needed.
|
||||||
|
|||||||
39
ci/dev/callbacks.dockerfile
Normal file
39
ci/dev/callbacks.dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Development Dockerfile for Callbacks Service with Air hot reload
|
||||||
|
|
||||||
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
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 && \
|
||||||
|
go install github.com/air-verse/air@latest
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY api/proto ./api/proto
|
||||||
|
COPY api/pkg ./api/pkg
|
||||||
|
COPY ci/scripts/proto/generate.sh ./ci/scripts/proto/
|
||||||
|
RUN bash ci/scripts/proto/generate.sh
|
||||||
|
|
||||||
|
# Runtime stage for development with Air
|
||||||
|
FROM golang:alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache bash git build-base && \
|
||||||
|
go install github.com/air-verse/air@latest
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy generated proto and pkg from builder
|
||||||
|
COPY --from=builder /src/api/proto ./api/proto
|
||||||
|
COPY --from=builder /src/api/pkg ./api/pkg
|
||||||
|
|
||||||
|
# Copy vault-aware entrypoint wrapper
|
||||||
|
COPY api/edge/callbacks/entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
# Source code will be mounted at runtime
|
||||||
|
WORKDIR /src/api/edge/callbacks
|
||||||
|
|
||||||
|
EXPOSE 9420
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
CMD ["air", "-c", ".air.toml", "--", "-config.file", "/app/config.yml", "-debug"]
|
||||||
20
ci/dev/vault-agent/callbacks.hcl
Normal file
20
ci/dev/vault-agent/callbacks.hcl
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
vault {
|
||||||
|
address = "http://dev-vault:8200"
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_auth {
|
||||||
|
method "approle" {
|
||||||
|
mount_path = "auth/approle"
|
||||||
|
config = {
|
||||||
|
role_id_file_path = "/run/vault/role_id"
|
||||||
|
secret_id_file_path = "/run/vault/secret_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sink "file" {
|
||||||
|
config = {
|
||||||
|
path = "/run/vault/token"
|
||||||
|
mode = 0600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -182,6 +182,20 @@ BFF_COMPOSE_PROJECT=sendico-bff
|
|||||||
BFF_SERVICE_NAME=sendico_bff
|
BFF_SERVICE_NAME=sendico_bff
|
||||||
BFF_HTTP_PORT=8080
|
BFF_HTTP_PORT=8080
|
||||||
|
|
||||||
|
# Callbacks service
|
||||||
|
CALLBACKS_DIR=callbacks
|
||||||
|
CALLBACKS_COMPOSE_PROJECT=sendico-callbacks
|
||||||
|
CALLBACKS_SERVICE_NAME=sendico_callbacks
|
||||||
|
CALLBACKS_METRICS_PORT=9420
|
||||||
|
CALLBACKS_VAULT_SECRET_PATH=sendico/edge/callbacks/vault
|
||||||
|
|
||||||
|
# Callbacks Mongo settings
|
||||||
|
CALLBACKS_MONGO_HOST=sendico_db1
|
||||||
|
CALLBACKS_MONGO_PORT=27017
|
||||||
|
CALLBACKS_MONGO_DATABASE=callbacks
|
||||||
|
CALLBACKS_MONGO_AUTH_SOURCE=admin
|
||||||
|
CALLBACKS_MONGO_REPLICA_SET=sendico-rs
|
||||||
|
|
||||||
# Chain gateway stack
|
# Chain gateway stack
|
||||||
CHAIN_GATEWAY_DIR=chain_gateway
|
CHAIN_GATEWAY_DIR=chain_gateway
|
||||||
CHAIN_GATEWAY_COMPOSE_PROJECT=sendico-chain-gateway
|
CHAIN_GATEWAY_COMPOSE_PROJECT=sendico-chain-gateway
|
||||||
|
|||||||
42
ci/prod/compose/callbacks.dockerfile
Normal file
42
ci/prod/compose/callbacks.dockerfile
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 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/edge/callbacks
|
||||||
|
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/edge/callbacks/internal/appversion.Version=${APP_VERSION} \
|
||||||
|
-X github.com/tech/sendico/edge/callbacks/internal/appversion.Revision=${GIT_REV} \
|
||||||
|
-X github.com/tech/sendico/edge/callbacks/internal/appversion.Branch=${BUILD_BRANCH} \
|
||||||
|
-X github.com/tech/sendico/edge/callbacks/internal/appversion.BuildUser=${BUILD_USER} \
|
||||||
|
-X github.com/tech/sendico/edge/callbacks/internal/appversion.BuildDate=${BUILD_DATE}" \
|
||||||
|
-o /out/callbacks .
|
||||||
|
|
||||||
|
FROM alpine:latest AS runtime
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata wget
|
||||||
|
WORKDIR /app
|
||||||
|
COPY api/edge/callbacks/config.yml /app/config.yml
|
||||||
|
COPY api/edge/callbacks/entrypoint.sh /app/entrypoint.sh
|
||||||
|
COPY --from=build /out/callbacks /app/callbacks
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
EXPOSE 9420
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
CMD ["/app/callbacks","--config.file","/app/config.yml"]
|
||||||
88
ci/prod/compose/callbacks.yml
Normal file
88
ci/prod/compose/callbacks.yml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Compose v2 - Edge Callbacks
|
||||||
|
|
||||||
|
x-common-env: &common-env
|
||||||
|
env_file:
|
||||||
|
- ../env/.env.runtime
|
||||||
|
- ../env/.env.version
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
callbacks-vault-run:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: tmpfs
|
||||||
|
device: tmpfs
|
||||||
|
o: size=8m,uid=0,gid=0,mode=0700
|
||||||
|
|
||||||
|
networks:
|
||||||
|
sendico-net:
|
||||||
|
external: true
|
||||||
|
name: sendico-net
|
||||||
|
|
||||||
|
services:
|
||||||
|
sendico_callbacks:
|
||||||
|
<<: *common-env
|
||||||
|
container_name: sendico-callbacks
|
||||||
|
restart: unless-stopped
|
||||||
|
image: ${REGISTRY_URL}/edge/callbacks:${APP_V}
|
||||||
|
pull_policy: always
|
||||||
|
environment:
|
||||||
|
CALLBACKS_MONGO_HOST: ${CALLBACKS_MONGO_HOST}
|
||||||
|
CALLBACKS_MONGO_PORT: ${CALLBACKS_MONGO_PORT}
|
||||||
|
CALLBACKS_MONGO_DATABASE: ${CALLBACKS_MONGO_DATABASE}
|
||||||
|
CALLBACKS_MONGO_USER: ${CALLBACKS_MONGO_USER}
|
||||||
|
CALLBACKS_MONGO_PASSWORD: ${CALLBACKS_MONGO_PASSWORD}
|
||||||
|
CALLBACKS_MONGO_AUTH_SOURCE: ${CALLBACKS_MONGO_AUTH_SOURCE}
|
||||||
|
CALLBACKS_MONGO_REPLICA_SET: ${CALLBACKS_MONGO_REPLICA_SET}
|
||||||
|
NATS_URL: ${NATS_URL}
|
||||||
|
NATS_HOST: ${NATS_HOST}
|
||||||
|
NATS_PORT: ${NATS_PORT}
|
||||||
|
NATS_USER: ${NATS_USER}
|
||||||
|
NATS_PASSWORD: ${NATS_PASSWORD}
|
||||||
|
CALLBACKS_METRICS_PORT: ${CALLBACKS_METRICS_PORT}
|
||||||
|
VAULT_ADDR: ${VAULT_ADDR}
|
||||||
|
VAULT_TOKEN_FILE: /run/vault/token
|
||||||
|
command: ["--config.file", "/app/config.yml"]
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:${CALLBACKS_METRICS_PORT}:${CALLBACKS_METRICS_PORT}"
|
||||||
|
volumes:
|
||||||
|
- callbacks-vault-run:/run/vault:ro
|
||||||
|
depends_on:
|
||||||
|
sendico_callbacks_vault_agent:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL","wget -qO- http://localhost:${CALLBACKS_METRICS_PORT}/health | grep -q '\"status\":\"ok\"'"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
networks:
|
||||||
|
- sendico-net
|
||||||
|
|
||||||
|
sendico_callbacks_vault_agent:
|
||||||
|
<<: *common-env
|
||||||
|
container_name: sendico-callbacks-vault-agent
|
||||||
|
restart: unless-stopped
|
||||||
|
image: hashicorp/vault:latest
|
||||||
|
pull_policy: always
|
||||||
|
cap_add: ["IPC_LOCK"]
|
||||||
|
environment:
|
||||||
|
VAULT_ADDR: ${VAULT_ADDR}
|
||||||
|
CALLBACKS_VAULT_ROLE_ID: ${CALLBACKS_VAULT_ROLE_ID}
|
||||||
|
CALLBACKS_VAULT_SECRET_ID: ${CALLBACKS_VAULT_SECRET_ID}
|
||||||
|
command: >
|
||||||
|
sh -lc 'set -euo pipefail; umask 077;
|
||||||
|
: "${CALLBACKS_VAULT_ROLE_ID:?}"; : "${CALLBACKS_VAULT_SECRET_ID:?}";
|
||||||
|
printf "%s" "$CALLBACKS_VAULT_ROLE_ID" > /run/vault/role_id;
|
||||||
|
printf "%s" "$CALLBACKS_VAULT_SECRET_ID" > /run/vault/secret_id;
|
||||||
|
unset CALLBACKS_VAULT_ROLE_ID CALLBACKS_VAULT_SECRET_ID;
|
||||||
|
exec vault agent -config=/etc/vault/agent/callbacks.hcl'
|
||||||
|
volumes:
|
||||||
|
- ./vault-agent/callbacks.hcl:/etc/vault/agent/callbacks.hcl:ro
|
||||||
|
- callbacks-vault-run:/run/vault
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD","test","-s","/run/vault/token"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
networks:
|
||||||
|
- sendico-net
|
||||||
20
ci/prod/compose/vault-agent/callbacks.hcl
Normal file
20
ci/prod/compose/vault-agent/callbacks.hcl
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
vault {
|
||||||
|
address = "https://vault.sendico.io"
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_auth {
|
||||||
|
method "approle" {
|
||||||
|
mount_path = "auth/approle"
|
||||||
|
config = {
|
||||||
|
role_id_file_path = "/run/vault/role_id"
|
||||||
|
secret_id_file_path = "/run/vault/secret_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sink "file" {
|
||||||
|
config = {
|
||||||
|
path = "/run/vault/token"
|
||||||
|
mode = 0600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
ci/prod/scripts/deploy/callbacks.sh
Executable file
157
ci/prod/scripts/deploy/callbacks.sh
Executable file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
[[ "${DEBUG_DEPLOY:-0}" = "1" ]] && set -x
|
||||||
|
trap 'echo "[deploy-callbacks] error at line $LINENO" >&2' ERR
|
||||||
|
|
||||||
|
: "${REMOTE_BASE:?missing REMOTE_BASE}"
|
||||||
|
: "${SSH_USER:?missing SSH_USER}"
|
||||||
|
: "${SSH_HOST:?missing SSH_HOST}"
|
||||||
|
: "${CALLBACKS_DIR:?missing CALLBACKS_DIR}"
|
||||||
|
: "${CALLBACKS_COMPOSE_PROJECT:?missing CALLBACKS_COMPOSE_PROJECT}"
|
||||||
|
: "${CALLBACKS_SERVICE_NAME:?missing CALLBACKS_SERVICE_NAME}"
|
||||||
|
|
||||||
|
REMOTE_DIR="${REMOTE_BASE%/}/${CALLBACKS_DIR}"
|
||||||
|
REMOTE_TARGET="${SSH_USER}@${SSH_HOST}"
|
||||||
|
COMPOSE_FILE="callbacks.yml"
|
||||||
|
SERVICE_NAMES="${CALLBACKS_SERVICE_NAME}"
|
||||||
|
|
||||||
|
REQUIRED_SECRETS=(
|
||||||
|
CALLBACKS_MONGO_USER
|
||||||
|
CALLBACKS_MONGO_PASSWORD
|
||||||
|
CALLBACKS_VAULT_ROLE_ID
|
||||||
|
CALLBACKS_VAULT_SECRET_ID
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
CALLBACKS_MONGO_USER_B64="$(b64enc "${CALLBACKS_MONGO_USER}")"
|
||||||
|
CALLBACKS_MONGO_PASSWORD_B64="$(b64enc "${CALLBACKS_MONGO_PASSWORD}")"
|
||||||
|
CALLBACKS_VAULT_ROLE_ID_B64="$(b64enc "${CALLBACKS_VAULT_ROLE_ID}")"
|
||||||
|
CALLBACKS_VAULT_SECRET_ID_B64="$(b64enc "${CALLBACKS_VAULT_SECRET_ID}")"
|
||||||
|
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="$CALLBACKS_COMPOSE_PROJECT" \
|
||||||
|
SERVICES_LINE="$SERVICES_LINE" \
|
||||||
|
CALLBACKS_MONGO_USER_B64="$CALLBACKS_MONGO_USER_B64" \
|
||||||
|
CALLBACKS_MONGO_PASSWORD_B64="$CALLBACKS_MONGO_PASSWORD_B64" \
|
||||||
|
CALLBACKS_VAULT_ROLE_ID_B64="$CALLBACKS_VAULT_ROLE_ID_B64" \
|
||||||
|
CALLBACKS_VAULT_SECRET_ID_B64="$CALLBACKS_VAULT_SECRET_ID_B64" \
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
|
||||||
|
CALLBACKS_MONGO_USER="$(decode_b64 "$CALLBACKS_MONGO_USER_B64")"
|
||||||
|
CALLBACKS_MONGO_PASSWORD="$(decode_b64 "$CALLBACKS_MONGO_PASSWORD_B64")"
|
||||||
|
CALLBACKS_VAULT_ROLE_ID="$(decode_b64 "$CALLBACKS_VAULT_ROLE_ID_B64")"
|
||||||
|
CALLBACKS_VAULT_SECRET_ID="$(decode_b64 "$CALLBACKS_VAULT_SECRET_ID_B64")"
|
||||||
|
NATS_USER="$(decode_b64 "$NATS_USER_B64")"
|
||||||
|
NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")"
|
||||||
|
NATS_URL="$(decode_b64 "$NATS_URL_B64")"
|
||||||
|
|
||||||
|
export CALLBACKS_MONGO_USER CALLBACKS_MONGO_PASSWORD
|
||||||
|
export CALLBACKS_VAULT_ROLE_ID CALLBACKS_VAULT_SECRET_ID
|
||||||
|
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/callbacks/build-image.sh
Executable file
85
ci/scripts/callbacks/build-image.sh
Executable 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
CALLBACKS_ENV_NAME="${CALLBACKS_ENV:-prod}"
|
||||||
|
RUNTIME_ENV_FILE="./ci/${CALLBACKS_ENV_NAME}/.env.runtime"
|
||||||
|
|
||||||
|
if [ ! -f "${RUNTIME_ENV_FILE}" ]; then
|
||||||
|
echo "[callbacks-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}"
|
||||||
|
CALLBACKS_DOCKERFILE="${CALLBACKS_DOCKERFILE:?missing CALLBACKS_DOCKERFILE}"
|
||||||
|
CALLBACKS_IMAGE_PATH="${CALLBACKS_IMAGE_PATH:?missing CALLBACKS_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="${CALLBACKS_BUILD_CONTEXT:-${WOODPECKER_WORKSPACE:-${CI_WORKSPACE:-${PWD:-/workspace}}}}"
|
||||||
|
if [ ! -d "${BUILD_CONTEXT}" ]; then
|
||||||
|
BUILD_CONTEXT="/workspace"
|
||||||
|
fi
|
||||||
|
|
||||||
|
/kaniko/executor \
|
||||||
|
--context "${BUILD_CONTEXT}" \
|
||||||
|
--dockerfile "${CALLBACKS_DOCKERFILE}" \
|
||||||
|
--destination "${REGISTRY_URL}/${CALLBACKS_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
|
||||||
66
ci/scripts/callbacks/deploy.sh
Executable file
66
ci/scripts/callbacks/deploy.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/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"
|
||||||
|
}
|
||||||
|
|
||||||
|
. ci/scripts/common/nats_env.sh
|
||||||
|
|
||||||
|
CALLBACKS_ENV_NAME="${CALLBACKS_ENV:-prod}"
|
||||||
|
RUNTIME_ENV_FILE="./ci/${CALLBACKS_ENV_NAME}/.env.runtime"
|
||||||
|
|
||||||
|
if [ ! -f "${RUNTIME_ENV_FILE}" ]; then
|
||||||
|
echo "[callbacks-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
|
||||||
|
|
||||||
|
CALLBACKS_MONGO_SECRET_PATH="${CALLBACKS_MONGO_SECRET_PATH:?missing CALLBACKS_MONGO_SECRET_PATH}"
|
||||||
|
CALLBACKS_VAULT_SECRET_PATH="${CALLBACKS_VAULT_SECRET_PATH:?missing CALLBACKS_VAULT_SECRET_PATH}"
|
||||||
|
|
||||||
|
export CALLBACKS_MONGO_USER="$(./ci/vlt kv_get kv "${CALLBACKS_MONGO_SECRET_PATH}" user)"
|
||||||
|
export CALLBACKS_MONGO_PASSWORD="$(./ci/vlt kv_get kv "${CALLBACKS_MONGO_SECRET_PATH}" password)"
|
||||||
|
export CALLBACKS_VAULT_ROLE_ID="$(./ci/vlt kv_get kv "${CALLBACKS_VAULT_SECRET_PATH}" role_id)"
|
||||||
|
export CALLBACKS_VAULT_SECRET_ID="$(./ci/vlt kv_get kv "${CALLBACKS_VAULT_SECRET_PATH}" secret_id)"
|
||||||
|
if [ -z "${CALLBACKS_VAULT_ROLE_ID}" ] || [ -z "${CALLBACKS_VAULT_SECRET_ID}" ]; then
|
||||||
|
echo "[callbacks-deploy] vault approle creds are empty for path ${CALLBACKS_VAULT_SECRET_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
load_nats_env
|
||||||
|
|
||||||
|
bash ci/prod/scripts/bootstrap/network.sh
|
||||||
|
bash ci/prod/scripts/deploy/callbacks.sh
|
||||||
@@ -33,6 +33,12 @@ case "${SERVICE}" in
|
|||||||
modules="
|
modules="
|
||||||
api/pkg
|
api/pkg
|
||||||
api/edge/bff
|
api/edge/bff
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
callbacks)
|
||||||
|
modules="
|
||||||
|
api/pkg
|
||||||
|
api/edge/callbacks
|
||||||
"
|
"
|
||||||
;;
|
;;
|
||||||
billing_documents)
|
billing_documents)
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ volumes:
|
|||||||
type: tmpfs
|
type: tmpfs
|
||||||
device: tmpfs
|
device: tmpfs
|
||||||
o: size=8m,uid=0,gid=0,mode=0700
|
o: size=8m,uid=0,gid=0,mode=0700
|
||||||
|
dev-callbacks-vault-run:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: tmpfs
|
||||||
|
device: tmpfs
|
||||||
|
o: size=8m,uid=0,gid=0,mode=0700
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# INFRASTRUCTURE SERVICES
|
# INFRASTRUCTURE SERVICES
|
||||||
@@ -843,6 +849,80 @@ services:
|
|||||||
MONGO_AUTH_SOURCE: admin
|
MONGO_AUTH_SOURCE: admin
|
||||||
MONGO_REPLICA_SET: dev-rs
|
MONGO_REPLICA_SET: dev-rs
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Callbacks Vault Agent (sidecar for AppRole authentication)
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
dev-callbacks-vault-agent:
|
||||||
|
<<: *common-env
|
||||||
|
image: hashicorp/vault:latest
|
||||||
|
container_name: dev-callbacks-vault-agent
|
||||||
|
restart: unless-stopped
|
||||||
|
cap_add: ["IPC_LOCK"]
|
||||||
|
environment:
|
||||||
|
VAULT_ADDR: ${VAULT_ADDR}
|
||||||
|
CALLBACKS_VAULT_ROLE_ID: ${CALLBACKS_VAULT_ROLE_ID}
|
||||||
|
CALLBACKS_VAULT_SECRET_ID: ${CALLBACKS_VAULT_SECRET_ID}
|
||||||
|
command: >
|
||||||
|
sh -c 'set -eu; umask 077;
|
||||||
|
: "$$CALLBACKS_VAULT_ROLE_ID"; : "$$CALLBACKS_VAULT_SECRET_ID";
|
||||||
|
echo "$$CALLBACKS_VAULT_ROLE_ID" > /run/vault/role_id;
|
||||||
|
echo "$$CALLBACKS_VAULT_SECRET_ID" > /run/vault/secret_id;
|
||||||
|
unset CALLBACKS_VAULT_ROLE_ID CALLBACKS_VAULT_SECRET_ID;
|
||||||
|
exec vault agent -config=/etc/vault/agent/callbacks.hcl'
|
||||||
|
volumes:
|
||||||
|
- ./ci/dev/vault-agent/callbacks.hcl:/etc/vault/agent/callbacks.hcl:ro
|
||||||
|
- dev-callbacks-vault-run:/run/vault
|
||||||
|
depends_on:
|
||||||
|
dev-vault: { condition: service_healthy }
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "test", "-s", "/run/vault/token"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
networks:
|
||||||
|
- sendico-dev
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Callbacks Service
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
dev-callbacks:
|
||||||
|
<<: *common-env
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ci/dev/callbacks.dockerfile
|
||||||
|
image: sendico-dev/callbacks:latest
|
||||||
|
container_name: dev-callbacks
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
dev-mongo-init: { condition: service_completed_successfully }
|
||||||
|
dev-nats: { condition: service_started }
|
||||||
|
dev-vault: { condition: service_healthy }
|
||||||
|
dev-callbacks-vault-agent: { condition: service_healthy }
|
||||||
|
volumes:
|
||||||
|
- ./api/edge/callbacks:/src/api/edge/callbacks
|
||||||
|
- ./api/edge/callbacks/config.dev.yml:/app/config.yml:ro
|
||||||
|
- dev-callbacks-vault-run:/run/vault:ro
|
||||||
|
ports:
|
||||||
|
- "9420:9420"
|
||||||
|
networks:
|
||||||
|
- sendico-dev
|
||||||
|
environment:
|
||||||
|
CALLBACKS_MONGO_HOST: dev-mongo-1
|
||||||
|
CALLBACKS_MONGO_PORT: 27017
|
||||||
|
CALLBACKS_MONGO_DATABASE: callbacks
|
||||||
|
CALLBACKS_MONGO_USER: ${MONGO_USER}
|
||||||
|
CALLBACKS_MONGO_PASSWORD: ${MONGO_PASSWORD}
|
||||||
|
CALLBACKS_MONGO_AUTH_SOURCE: admin
|
||||||
|
CALLBACKS_MONGO_REPLICA_SET: dev-rs
|
||||||
|
NATS_HOST: dev-nats
|
||||||
|
NATS_PORT: 4222
|
||||||
|
NATS_USER: ${NATS_USER}
|
||||||
|
NATS_PASSWORD: ${NATS_PASSWORD}
|
||||||
|
NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222
|
||||||
|
CALLBACKS_METRICS_PORT: 9420
|
||||||
|
VAULT_ADDR: ${VAULT_ADDR}
|
||||||
|
VAULT_TOKEN_FILE: /run/vault/token
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# BFF (Backend for Frontend / Server) Service
|
# BFF (Backend for Frontend / Server) Service
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user