mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
178 lines
5.4 KiB
Go
178 lines
5.4 KiB
Go
package connectors
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
|
|
pluginserrors "github.com/hanzo-fi/payments/internal/connectors/plugins"
|
|
"github.com/hanzo-fi/payments/internal/connectors/plugins/registry"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var (
|
|
ErrNotFound = errors.New("plugin not loaded in manager")
|
|
ErrValidation = errors.New("validation error")
|
|
ErrPollingPeriod = errors.New("polling period invalid")
|
|
ErrInvalidOperation = errors.New("invalid operation")
|
|
)
|
|
|
|
//go:generate mockgen -source manager.go -destination manager_generated.go -package connectors . Manager
|
|
type Manager interface {
|
|
Load(models.Connector, bool, bool) (string, json.RawMessage, error)
|
|
Unload(models.ConnectorID)
|
|
GetConfig(models.ConnectorID) (models.Config, error)
|
|
Get(models.ConnectorID) (models.Plugin, error)
|
|
// DefaultConfig returns the configurer's default connector config. It carries
|
|
// no per-connector state (no lookup, never ErrNotFound) and is used to apply
|
|
// the same defaults the manager applies when parsing a stored config.
|
|
DefaultConfig() models.Config
|
|
}
|
|
|
|
// Will start, hold, manage and stop Connectors
|
|
type manager struct {
|
|
logger logging.Logger
|
|
|
|
connectors map[string]connector
|
|
rwMutex sync.RWMutex
|
|
|
|
configurer *Configurer
|
|
|
|
debug bool
|
|
}
|
|
|
|
type connector struct {
|
|
plugin models.Plugin
|
|
config models.Config
|
|
|
|
validatedConfigJson json.RawMessage
|
|
}
|
|
|
|
func NewManager(
|
|
logger logging.Logger,
|
|
debug bool,
|
|
pollingPeriodDefault time.Duration,
|
|
pollingPeriodMinimum time.Duration,
|
|
) *manager {
|
|
configurer, err := NewConfigurer(pollingPeriodDefault, pollingPeriodMinimum)
|
|
if err != nil {
|
|
// NewManager is only expected to be called in modules - we'd rather fail starting the app than
|
|
// start it misconfigured
|
|
log.Panicf("invalid connector polling period configuration: %v", err)
|
|
}
|
|
return &manager{
|
|
logger: logger,
|
|
configurer: configurer,
|
|
connectors: make(map[string]connector),
|
|
debug: debug,
|
|
}
|
|
}
|
|
|
|
func (m *manager) Load(connectorModel models.Connector, updateExisting bool, strictValidation bool) (configName string, validatedConfigJson json.RawMessage, err error) {
|
|
m.rwMutex.Lock()
|
|
defer m.rwMutex.Unlock()
|
|
|
|
config := m.configurer.DefaultConfig()
|
|
if err := json.Unmarshal(connectorModel.Config, &config); err != nil {
|
|
// A malformed JSON body is a client error: classify it as invalid config
|
|
// so install/update surfaces a 400 rather than a 500.
|
|
return "", nil, fmt.Errorf("%w: %w", models.ErrInvalidConfig, err)
|
|
}
|
|
|
|
// Check if plugin is already installed
|
|
_, ok := m.connectors[connectorModel.ID.String()]
|
|
if ok && !updateExisting {
|
|
return config.Name, m.connectors[connectorModel.ID.String()].validatedConfigJson, nil
|
|
}
|
|
|
|
if err := m.configurer.Validate(config); err != nil {
|
|
if !errors.Is(err, ErrPollingPeriod) {
|
|
return "", nil, err
|
|
}
|
|
|
|
// strict validation takes place on install/update but not when launching a new instance of the app
|
|
// which is only loading a presumably already validated value from the DB
|
|
if strictValidation {
|
|
return "", nil, fmt.Errorf("%w: %w", models.ErrInvalidConfig, err)
|
|
}
|
|
// if the polling period is lower that the current system default we should still load the plugin
|
|
// since creating validation errors will not change the schedule in temporal
|
|
m.logger.Errorf("connector %q has a low polling period of %s", connectorModel.ID.String(), config.PollingPeriod)
|
|
}
|
|
|
|
plugin, err := registry.GetPlugin(connectorModel.ID, m.logger, connectorModel.Provider, config.Name, connectorModel.Config)
|
|
switch {
|
|
case errors.Is(err, pluginserrors.ErrNotImplemented),
|
|
errors.Is(err, pluginserrors.ErrInvalidClientRequest),
|
|
// An unknown or Enterprise-only provider in an install/update request is a
|
|
// client error, not a server error: surface it as a 400 rather than a 500.
|
|
errors.Is(err, registry.ErrPluginNotFound),
|
|
errors.Is(err, registry.ErrPluginEnterpriseOnly):
|
|
return "", nil, fmt.Errorf("%w: %w", err, ErrValidation)
|
|
case err != nil:
|
|
return "", nil, err
|
|
}
|
|
|
|
b, err := combineConfigs(config, plugin.Config())
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to combine configs: %w", err)
|
|
}
|
|
|
|
plugin.ScheduleForDeletion(connectorModel.ScheduledForDeletion)
|
|
|
|
validatedConfigJson = json.RawMessage(b)
|
|
m.connectors[connectorModel.ID.String()] = connector{
|
|
plugin: plugin,
|
|
config: config,
|
|
validatedConfigJson: validatedConfigJson,
|
|
}
|
|
return config.Name, validatedConfigJson, nil
|
|
}
|
|
|
|
func (m *manager) Unload(connectorID models.ConnectorID) {
|
|
m.rwMutex.Lock()
|
|
defer m.rwMutex.Unlock()
|
|
|
|
_, ok := m.connectors[connectorID.String()]
|
|
if !ok {
|
|
// Nothing to do
|
|
return
|
|
}
|
|
|
|
delete(m.connectors, connectorID.String())
|
|
}
|
|
|
|
func (m *manager) Get(connectorID models.ConnectorID) (models.Plugin, error) {
|
|
m.rwMutex.RLock()
|
|
defer m.rwMutex.RUnlock()
|
|
|
|
c, ok := m.connectors[connectorID.String()]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: %w", connectorID.String(), ErrNotFound)
|
|
}
|
|
|
|
return c.plugin, nil
|
|
}
|
|
|
|
func (m *manager) DefaultConfig() models.Config {
|
|
return m.configurer.DefaultConfig()
|
|
}
|
|
|
|
func (m *manager) GetConfig(connectorID models.ConnectorID) (models.Config, error) {
|
|
m.rwMutex.RLock()
|
|
defer m.rwMutex.RUnlock()
|
|
|
|
c, ok := m.connectors[connectorID.String()]
|
|
if !ok {
|
|
return models.Config{}, fmt.Errorf("%s: %w", connectorID.String(), ErrNotFound)
|
|
}
|
|
|
|
return c.config, nil
|
|
}
|
|
|
|
var _ Manager = &manager{}
|