483 lines
13 KiB
Go
483 lines
13 KiB
Go
package apiimp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tech/sendico/pkg/discovery"
|
|
msg "github.com/tech/sendico/pkg/messaging"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
eapi "github.com/tech/sendico/server/interface/api"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
discoveryBootstrapTimeout = 3 * time.Second
|
|
discoveryBootstrapSender = "server_bootstrap"
|
|
defaultClientDialTimeoutSecs = 5
|
|
defaultClientCallTimeoutSecs = 5
|
|
)
|
|
|
|
var (
|
|
ledgerDiscoveryServiceNames = []string{
|
|
"LEDGER",
|
|
string(mservice.Ledger),
|
|
}
|
|
paymentOrchestratorDiscoveryServiceNames = []string{
|
|
"PAYMENTS_ORCHESTRATOR",
|
|
string(mservice.PaymentOrchestrator),
|
|
}
|
|
paymentQuotationDiscoveryServiceNames = []string{
|
|
"PAYMENTS_QUOTATION",
|
|
"PAYMENTS_QUOTE",
|
|
"PAYMENT_QUOTATION",
|
|
"payment_quotation",
|
|
}
|
|
paymentMethodsDiscoveryServiceNames = []string{
|
|
"PAYMENTS_METHODS",
|
|
"PAYMENT_METHODS",
|
|
string(mservice.PaymentMethods),
|
|
}
|
|
)
|
|
|
|
type discoveryEndpoint struct {
|
|
address string
|
|
insecure bool
|
|
raw string
|
|
}
|
|
|
|
type serviceSelection struct {
|
|
service discovery.ServiceSummary
|
|
endpoint discoveryEndpoint
|
|
opMatch bool
|
|
nameRank int
|
|
}
|
|
|
|
type gatewaySelection struct {
|
|
gateway discovery.GatewaySummary
|
|
endpoint discoveryEndpoint
|
|
networkMatch bool
|
|
opMatch bool
|
|
}
|
|
|
|
// resolveServiceAddressesFromDiscovery looks up downstream service addresses once
|
|
// during startup and applies them to the runtime config.
|
|
func (a *APIImp) resolveServiceAddressesFromDiscovery() {
|
|
if a == nil || a.config == nil || a.config.Mw == nil {
|
|
return
|
|
}
|
|
|
|
msgCfg := a.config.Mw.Messaging
|
|
if msgCfg.Driver == "" {
|
|
return
|
|
}
|
|
|
|
logger := a.logger.Named("discovery_bootstrap")
|
|
broker, err := msg.CreateMessagingBroker(logger.Named("bus"), &msgCfg)
|
|
if err != nil {
|
|
logger.Warn("Failed to create discovery bootstrap broker", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
client, err := discovery.NewClient(logger, broker, nil, discoveryBootstrapSender)
|
|
if err != nil {
|
|
logger.Warn("Failed to create discovery bootstrap client", zap.Error(err))
|
|
return
|
|
}
|
|
defer client.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), discoveryBootstrapTimeout)
|
|
defer cancel()
|
|
|
|
lookup, err := client.Lookup(ctx)
|
|
if err != nil {
|
|
logger.Warn("Failed to fetch discovery registry during startup", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
a.resolveChainGatewayAddress(lookup.Gateways)
|
|
orchestratorFound, orchestratorEndpoint := a.resolvePaymentOrchestratorAddress(lookup.Services)
|
|
a.resolveLedgerAddress(lookup.Services)
|
|
a.resolvePaymentQuotationAddress(lookup.Services, orchestratorFound, orchestratorEndpoint)
|
|
a.resolvePaymentMethodsAddress(lookup.Services)
|
|
}
|
|
|
|
func (a *APIImp) resolveChainGatewayAddress(gateways []discovery.GatewaySummary) {
|
|
cfg := a.config.ChainGateway
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
|
|
endpoint, selected, ok := selectGatewayEndpoint(
|
|
gateways,
|
|
cfg.DefaultAsset.Chain,
|
|
[]string{discovery.OperationBalanceRead},
|
|
)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
cfg.Address = endpoint.address
|
|
cfg.Insecure = endpoint.insecure
|
|
ensureTimeoutsChainGateway(cfg)
|
|
|
|
a.logger.Info("Resolved chain gateway address from discovery",
|
|
zap.String("rail", selected.Rail),
|
|
zap.String("gateway_id", selected.ID),
|
|
zap.String("network", selected.Network),
|
|
zap.String("invoke_uri", endpoint.raw),
|
|
zap.String("address", endpoint.address),
|
|
zap.Bool("insecure", endpoint.insecure))
|
|
}
|
|
|
|
func (a *APIImp) resolveLedgerAddress(services []discovery.ServiceSummary) {
|
|
endpoint, selected, ok := selectServiceEndpoint(
|
|
services,
|
|
ledgerDiscoveryServiceNames,
|
|
discovery.LedgerServiceOperations(),
|
|
)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
cfg := ensureLedgerConfig(a.config)
|
|
cfg.Address = endpoint.address
|
|
cfg.Insecure = endpoint.insecure
|
|
ensureTimeoutsLedger(cfg)
|
|
|
|
a.logger.Info("Resolved ledger address from discovery",
|
|
zap.String("service", selected.Service),
|
|
zap.String("service_id", selected.ID),
|
|
zap.String("instance_id", selected.InstanceID),
|
|
zap.String("invoke_uri", endpoint.raw),
|
|
zap.String("address", endpoint.address),
|
|
zap.Bool("insecure", endpoint.insecure))
|
|
}
|
|
|
|
func (a *APIImp) resolvePaymentOrchestratorAddress(services []discovery.ServiceSummary) (bool, discoveryEndpoint) {
|
|
endpoint, selected, ok := selectServiceEndpoint(
|
|
services,
|
|
paymentOrchestratorDiscoveryServiceNames,
|
|
[]string{discovery.OperationPaymentInitiate},
|
|
)
|
|
if !ok {
|
|
return false, discoveryEndpoint{}
|
|
}
|
|
|
|
cfg := ensurePaymentOrchestratorConfig(a.config)
|
|
cfg.Address = endpoint.address
|
|
cfg.Insecure = endpoint.insecure
|
|
ensureTimeoutsPayment(cfg)
|
|
|
|
a.logger.Info("Resolved payment orchestrator address from discovery",
|
|
zap.String("service", selected.Service),
|
|
zap.String("service_id", selected.ID),
|
|
zap.String("instance_id", selected.InstanceID),
|
|
zap.String("invoke_uri", endpoint.raw),
|
|
zap.String("address", endpoint.address),
|
|
zap.Bool("insecure", endpoint.insecure))
|
|
|
|
return true, endpoint
|
|
}
|
|
|
|
func (a *APIImp) resolvePaymentQuotationAddress(services []discovery.ServiceSummary, orchestratorFound bool, orchestratorEndpoint discoveryEndpoint) {
|
|
endpoint, selected, ok := selectServiceEndpoint(
|
|
services,
|
|
paymentQuotationDiscoveryServiceNames,
|
|
[]string{discovery.OperationPaymentQuote},
|
|
)
|
|
if !ok {
|
|
cfg := a.config.PaymentQuotation
|
|
if cfg != nil && strings.TrimSpace(cfg.Address) != "" {
|
|
return
|
|
}
|
|
if !orchestratorFound {
|
|
return
|
|
}
|
|
// Fall back to orchestrator endpoint when quotation service is not announced.
|
|
endpoint = orchestratorEndpoint
|
|
selected = discovery.ServiceSummary{Service: "PAYMENTS_ORCHESTRATOR"}
|
|
}
|
|
|
|
cfg := ensurePaymentQuotationConfig(a.config)
|
|
cfg.Address = endpoint.address
|
|
cfg.Insecure = endpoint.insecure
|
|
ensureTimeoutsPayment(cfg)
|
|
|
|
a.logger.Info("Resolved payment quotation address from discovery",
|
|
zap.String("service", selected.Service),
|
|
zap.String("service_id", selected.ID),
|
|
zap.String("instance_id", selected.InstanceID),
|
|
zap.String("invoke_uri", endpoint.raw),
|
|
zap.String("address", endpoint.address),
|
|
zap.Bool("insecure", endpoint.insecure))
|
|
}
|
|
|
|
func (a *APIImp) resolvePaymentMethodsAddress(services []discovery.ServiceSummary) {
|
|
endpoint, selected, ok := selectServiceEndpoint(
|
|
services,
|
|
paymentMethodsDiscoveryServiceNames,
|
|
[]string{discovery.OperationPaymentMethodsRead},
|
|
)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
cfg := ensurePaymentMethodsConfig(a.config)
|
|
cfg.Address = endpoint.address
|
|
cfg.Insecure = endpoint.insecure
|
|
ensureTimeoutsPayment(cfg)
|
|
|
|
a.logger.Info("Resolved payment methods address from discovery",
|
|
zap.String("service", selected.Service),
|
|
zap.String("service_id", selected.ID),
|
|
zap.String("instance_id", selected.InstanceID),
|
|
zap.String("invoke_uri", endpoint.raw),
|
|
zap.String("address", endpoint.address),
|
|
zap.Bool("insecure", endpoint.insecure))
|
|
}
|
|
|
|
func selectServiceEndpoint(services []discovery.ServiceSummary, serviceNames []string, requiredOps []string) (discoveryEndpoint, discovery.ServiceSummary, bool) {
|
|
selections := make([]serviceSelection, 0)
|
|
for _, svc := range services {
|
|
if !svc.Healthy {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(svc.InvokeURI) == "" {
|
|
continue
|
|
}
|
|
nameRank, ok := serviceRank(svc.Service, serviceNames)
|
|
if !ok {
|
|
continue
|
|
}
|
|
endpoint, err := parseDiscoveryInvokeURI(svc.InvokeURI)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
selections = append(selections, serviceSelection{
|
|
service: svc,
|
|
endpoint: endpoint,
|
|
opMatch: discovery.HasAnyOperation(svc.Ops, requiredOps),
|
|
nameRank: nameRank,
|
|
})
|
|
}
|
|
if len(selections) == 0 {
|
|
return discoveryEndpoint{}, discovery.ServiceSummary{}, false
|
|
}
|
|
|
|
sort.Slice(selections, func(i, j int) bool {
|
|
if selections[i].opMatch != selections[j].opMatch {
|
|
return selections[i].opMatch
|
|
}
|
|
if selections[i].nameRank != selections[j].nameRank {
|
|
return selections[i].nameRank < selections[j].nameRank
|
|
}
|
|
if selections[i].service.ID != selections[j].service.ID {
|
|
return selections[i].service.ID < selections[j].service.ID
|
|
}
|
|
return selections[i].service.InstanceID < selections[j].service.InstanceID
|
|
})
|
|
|
|
selected := selections[0]
|
|
return selected.endpoint, selected.service, true
|
|
}
|
|
|
|
func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork string, requiredOps []string) (discoveryEndpoint, discovery.GatewaySummary, bool) {
|
|
preferredNetwork = strings.TrimSpace(preferredNetwork)
|
|
selections := make([]gatewaySelection, 0)
|
|
|
|
for _, gateway := range gateways {
|
|
if !gateway.Healthy {
|
|
continue
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(gateway.Rail), discovery.RailCrypto) {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(gateway.InvokeURI) == "" {
|
|
continue
|
|
}
|
|
endpoint, err := parseDiscoveryInvokeURI(gateway.InvokeURI)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
selections = append(selections, gatewaySelection{
|
|
gateway: gateway,
|
|
endpoint: endpoint,
|
|
networkMatch: preferredNetwork != "" && strings.EqualFold(strings.TrimSpace(gateway.Network), preferredNetwork),
|
|
opMatch: discovery.HasAnyOperation(gateway.Ops, requiredOps),
|
|
})
|
|
}
|
|
if len(selections) == 0 {
|
|
return discoveryEndpoint{}, discovery.GatewaySummary{}, false
|
|
}
|
|
|
|
sort.Slice(selections, func(i, j int) bool {
|
|
if selections[i].networkMatch != selections[j].networkMatch {
|
|
return selections[i].networkMatch
|
|
}
|
|
if selections[i].opMatch != selections[j].opMatch {
|
|
return selections[i].opMatch
|
|
}
|
|
if selections[i].gateway.RoutingPriority != selections[j].gateway.RoutingPriority {
|
|
return selections[i].gateway.RoutingPriority > selections[j].gateway.RoutingPriority
|
|
}
|
|
if selections[i].gateway.ID != selections[j].gateway.ID {
|
|
return selections[i].gateway.ID < selections[j].gateway.ID
|
|
}
|
|
return selections[i].gateway.InstanceID < selections[j].gateway.InstanceID
|
|
})
|
|
|
|
selected := selections[0]
|
|
return selected.endpoint, selected.gateway, true
|
|
}
|
|
|
|
func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return discoveryEndpoint{}, fmt.Errorf("Invoke uri is empty")
|
|
}
|
|
|
|
// Without a scheme we expect a plain host:port target.
|
|
if !strings.Contains(raw, "://") {
|
|
if _, _, err := net.SplitHostPort(raw); err != nil {
|
|
return discoveryEndpoint{}, fmt.Errorf("Invoke uri must include host:port: %w", err)
|
|
}
|
|
return discoveryEndpoint{
|
|
address: raw,
|
|
insecure: true,
|
|
raw: raw,
|
|
}, nil
|
|
}
|
|
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil {
|
|
return discoveryEndpoint{}, err
|
|
}
|
|
|
|
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
|
|
case "grpc":
|
|
address := strings.TrimSpace(parsed.Host)
|
|
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
|
return discoveryEndpoint{}, fmt.Errorf("Grpc invoke uri must include host:port: %w", splitErr)
|
|
}
|
|
return discoveryEndpoint{
|
|
address: address,
|
|
insecure: true,
|
|
raw: raw,
|
|
}, nil
|
|
case "grpcs":
|
|
address := strings.TrimSpace(parsed.Host)
|
|
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
|
return discoveryEndpoint{}, fmt.Errorf("Grpcs invoke uri must include host:port: %w", splitErr)
|
|
}
|
|
return discoveryEndpoint{
|
|
address: address,
|
|
insecure: false,
|
|
raw: raw,
|
|
}, nil
|
|
case "dns", "passthrough":
|
|
// gRPC resolver targets such as dns:///service:port.
|
|
return discoveryEndpoint{
|
|
address: raw,
|
|
insecure: true,
|
|
raw: raw,
|
|
}, nil
|
|
default:
|
|
return discoveryEndpoint{}, fmt.Errorf("Unsupported invoke uri scheme: %s", parsed.Scheme)
|
|
}
|
|
}
|
|
|
|
func serviceRank(service string, names []string) (int, bool) {
|
|
service = strings.TrimSpace(service)
|
|
if service == "" {
|
|
return 0, false
|
|
}
|
|
for i, name := range names {
|
|
if strings.EqualFold(service, strings.TrimSpace(name)) {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func ensureLedgerConfig(cfg *eapi.Config) *eapi.LedgerConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
if cfg.Ledger == nil {
|
|
cfg.Ledger = &eapi.LedgerConfig{}
|
|
}
|
|
return cfg.Ledger
|
|
}
|
|
|
|
func ensurePaymentOrchestratorConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
if cfg.PaymentOrchestrator == nil {
|
|
cfg.PaymentOrchestrator = &eapi.PaymentOrchestratorConfig{}
|
|
}
|
|
return cfg.PaymentOrchestrator
|
|
}
|
|
|
|
func ensurePaymentQuotationConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
if cfg.PaymentQuotation == nil {
|
|
cfg.PaymentQuotation = &eapi.PaymentOrchestratorConfig{}
|
|
}
|
|
return cfg.PaymentQuotation
|
|
}
|
|
|
|
func ensurePaymentMethodsConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
if cfg.PaymentMethods == nil {
|
|
cfg.PaymentMethods = &eapi.PaymentOrchestratorConfig{}
|
|
}
|
|
return cfg.PaymentMethods
|
|
}
|
|
|
|
func ensureTimeoutsLedger(cfg *eapi.LedgerConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.DialTimeoutSeconds <= 0 {
|
|
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
|
}
|
|
if cfg.CallTimeoutSeconds <= 0 {
|
|
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
|
}
|
|
}
|
|
|
|
func ensureTimeoutsChainGateway(cfg *eapi.ChainGatewayConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.DialTimeoutSeconds <= 0 {
|
|
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
|
}
|
|
if cfg.CallTimeoutSeconds <= 0 {
|
|
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
|
}
|
|
}
|
|
|
|
func ensureTimeoutsPayment(cfg *eapi.PaymentOrchestratorConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.DialTimeoutSeconds <= 0 {
|
|
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
|
}
|
|
if cfg.CallTimeoutSeconds <= 0 {
|
|
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
|
}
|
|
}
|