refactor: extract ProxyProvider interface with None and NPM implementations

Replace direct npm.Client usage throughout the codebase with the
proxy.Provider interface, enabling pluggable proxy backends. The
deployer, API layer, and proxy manager now use provider-agnostic
route management (ConfigureRoute/DeleteRoute) instead of NPM-specific
API calls. Adds ProxyRouteID (string) to Instance model and
ProxyProvider setting to Settings, with SQLite migrations for
backward compatibility.
This commit is contained in:
2026-04-04 19:39:08 +03:00
parent 6667abf03c
commit 7d6719da12
17 changed files with 365 additions and 249 deletions
+36 -62
View File
@@ -8,23 +8,22 @@ import (
"sync"
"github.com/alexei/docker-watcher/internal/dns"
"github.com/alexei/docker-watcher/internal/npm"
"github.com/alexei/docker-watcher/internal/store"
)
// Manager handles the lifecycle of standalone proxy hosts.
type Manager struct {
store *store.Store
npm *npm.Client
dnsMu sync.RWMutex
dns dns.Provider // nil when wildcard DNS is active
store *store.Store
provider Provider
dnsMu sync.RWMutex
dns dns.Provider // nil when wildcard DNS is active
}
// NewManager creates a new proxy manager.
func NewManager(st *store.Store, npmClient *npm.Client) *Manager {
func NewManager(st *store.Store, provider Provider) *Manager {
return &Manager{
store: st,
npm: npmClient,
store: st,
provider: provider,
}
}
@@ -70,7 +69,7 @@ type ProxyView struct {
CreatedAt string `json:"created_at"`
}
// CreateProxy validates the destination, creates an NPM proxy host, and saves to the store.
// CreateProxy validates the destination, creates a proxy route via the provider, and saves to the store.
func (m *Manager) CreateProxy(ctx context.Context, req CreateProxyRequest) (store.StandaloneProxy, error) {
// Validate destination.
result := ValidateDestination(ctx, req.DestinationURL, req.DestinationPort)
@@ -84,29 +83,16 @@ func (m *Manager) CreateProxy(ctx context.Context, req CreateProxyRequest) (stor
return store.StandaloneProxy{}, fmt.Errorf("get settings: %w", err)
}
// Build NPM proxy host config.
config := npm.ProxyHostConfig{
DomainNames: []string{req.Domain},
ForwardScheme: "http",
ForwardHost: req.DestinationURL,
ForwardPort: req.DestinationPort,
CertificateID: settings.SSLCertificateID,
SSLForced: settings.SSLCertificateID > 0,
BlockExploits: true,
AllowWebsocket: true,
HTTP2Support: true,
HSTSEnabled: settings.SSLCertificateID > 0,
Locations: []any{},
}
// Create NPM proxy host.
npmHost, err := m.npm.CreateProxyHost(ctx, config)
// Create proxy route via provider.
routeID, err := m.provider.ConfigureRoute(ctx, req.Domain, req.DestinationURL, req.DestinationPort, RouteOptions{
SSLCertificateID: settings.SSLCertificateID,
})
if err != nil {
return store.StandaloneProxy{}, fmt.Errorf("create NPM proxy host: %w", err)
return store.StandaloneProxy{}, fmt.Errorf("create proxy route: %w", err)
}
slog.Info("created NPM proxy host for standalone proxy",
"domain", req.Domain, "npm_proxy_id", npmHost.ID)
slog.Info("created proxy route for standalone proxy",
"domain", req.Domain, "route_id", routeID, "provider", m.provider.Name())
// Save to store.
proxy, err := m.store.CreateStandaloneProxy(store.StandaloneProxy{
@@ -114,14 +100,13 @@ func (m *Manager) CreateProxy(ctx context.Context, req CreateProxyRequest) (stor
DestinationURL: req.DestinationURL,
DestinationPort: req.DestinationPort,
SSLCertificateID: settings.SSLCertificateID,
NpmProxyID: npmHost.ID,
HealthStatus: "unknown",
})
if err != nil {
// Best effort: clean up the NPM host if store insert fails.
if delErr := m.npm.DeleteProxyHost(ctx, npmHost.ID); delErr != nil {
slog.Error("failed to clean up NPM proxy host after store error",
"npm_proxy_id", npmHost.ID, "error", delErr)
// Best effort: clean up the proxy route if store insert fails.
if delErr := m.provider.DeleteRoute(ctx, routeID); delErr != nil {
slog.Error("failed to clean up proxy route after store error",
"route_id", routeID, "error", delErr)
}
return store.StandaloneProxy{}, fmt.Errorf("save standalone proxy: %w", err)
}
@@ -132,7 +117,7 @@ func (m *Manager) CreateProxy(ctx context.Context, req CreateProxyRequest) (stor
return proxy, nil
}
// UpdateProxy re-validates the destination, updates the NPM proxy host, and updates the store.
// UpdateProxy re-validates the destination, updates the proxy route via the provider, and updates the store.
func (m *Manager) UpdateProxy(ctx context.Context, id string, req UpdateProxyRequest) (store.StandaloneProxy, error) {
existing, err := m.store.GetStandaloneProxy(id)
if err != nil {
@@ -151,23 +136,11 @@ func (m *Manager) UpdateProxy(ctx context.Context, id string, req UpdateProxyReq
return store.StandaloneProxy{}, fmt.Errorf("get settings: %w", err)
}
// Update NPM proxy host.
config := npm.ProxyHostConfig{
DomainNames: []string{req.Domain},
ForwardScheme: "http",
ForwardHost: req.DestinationURL,
ForwardPort: req.DestinationPort,
CertificateID: settings.SSLCertificateID,
SSLForced: settings.SSLCertificateID > 0,
BlockExploits: true,
AllowWebsocket: true,
HTTP2Support: true,
HSTSEnabled: settings.SSLCertificateID > 0,
Locations: []any{},
}
if _, err := m.npm.UpdateProxyHost(ctx, existing.NpmProxyID, config); err != nil {
return store.StandaloneProxy{}, fmt.Errorf("update NPM proxy host: %w", err)
// Update proxy route via provider (ConfigureRoute handles create-or-update).
if _, err := m.provider.ConfigureRoute(ctx, req.Domain, req.DestinationURL, req.DestinationPort, RouteOptions{
SSLCertificateID: settings.SSLCertificateID,
}); err != nil {
return store.StandaloneProxy{}, fmt.Errorf("update proxy route: %w", err)
}
// Update store.
@@ -191,23 +164,24 @@ func (m *Manager) UpdateProxy(ctx context.Context, id string, req UpdateProxyReq
return m.store.GetStandaloneProxy(id)
}
// DeleteProxy removes the NPM proxy host and deletes from the store.
// DeleteProxy removes the proxy route via the provider and deletes from the store.
func (m *Manager) DeleteProxy(ctx context.Context, id string) error {
proxy, err := m.store.GetStandaloneProxy(id)
p, err := m.store.GetStandaloneProxy(id)
if err != nil {
return fmt.Errorf("get proxy: %w", err)
}
// Delete NPM proxy host.
if proxy.NpmProxyID > 0 {
if err := m.npm.DeleteProxyHost(ctx, proxy.NpmProxyID); err != nil {
slog.Warn("failed to delete NPM proxy host (continuing with store deletion)",
"npm_proxy_id", proxy.NpmProxyID, "error", err)
// Delete proxy route via provider using the NpmProxyID as a string route ID.
if p.NpmProxyID > 0 {
routeID := fmt.Sprintf("%d", p.NpmProxyID)
if err := m.provider.DeleteRoute(ctx, routeID); err != nil {
slog.Warn("failed to delete proxy route (continuing with store deletion)",
"route_id", routeID, "error", err)
}
}
// Remove DNS record.
m.removeDNS(ctx, proxy.Domain)
m.removeDNS(ctx, p.Domain)
if err := m.store.DeleteStandaloneProxy(id); err != nil {
return fmt.Errorf("delete standalone proxy: %w", err)
@@ -257,7 +231,7 @@ func (m *Manager) ListAllProxies() ([]ProxyView, error) {
})
}
// Deploy-managed proxies: instances with npm_proxy_id > 0.
// Deploy-managed proxies: instances with a proxy route configured.
instances, err := m.store.ListAllInstances()
if err != nil {
return nil, fmt.Errorf("list instances: %w", err)
@@ -278,7 +252,7 @@ func (m *Manager) ListAllProxies() ([]ProxyView, error) {
}
for _, inst := range instances {
if inst.NpmProxyID <= 0 {
if inst.ProxyRouteID == "" && inst.NpmProxyID <= 0 {
continue
}