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
+118
View File
@@ -0,0 +1,118 @@
package proxy
import (
"context"
"fmt"
"strconv"
"github.com/alexei/docker-watcher/internal/npm"
)
// NpmProvider wraps the NPM client behind the Provider interface.
// It handles authentication transparently before each operation.
type NpmProvider struct {
client *npm.Client
email string
password string
}
// NewNpmProvider creates an NPM-backed proxy provider.
// The email and password are the decrypted NPM credentials.
func NewNpmProvider(client *npm.Client, email, password string) *NpmProvider {
return &NpmProvider{
client: client,
email: email,
password: password,
}
}
// UpdateCredentials updates the stored NPM credentials (e.g., after settings change).
func (p *NpmProvider) UpdateCredentials(email, password string) {
p.email = email
p.password = password
}
func (p *NpmProvider) Name() string { return "npm" }
func (p *NpmProvider) ConfigureRoute(ctx context.Context, domain, targetHost string, targetPort int, opts RouteOptions) (string, error) {
if err := p.auth(ctx); err != nil {
return "", err
}
scheme := opts.ForwardScheme
if scheme == "" {
scheme = "http"
}
// Check if a proxy host already exists for this domain.
existing, found, err := p.client.FindProxyHostByDomain(ctx, domain)
if err != nil {
return "", fmt.Errorf("find existing proxy host: %w", err)
}
config := npm.ProxyHostConfig{
DomainNames: []string{domain},
ForwardScheme: scheme,
ForwardHost: targetHost,
ForwardPort: targetPort,
BlockExploits: true,
AllowWebsocket: true,
HTTP2Support: true,
Meta: npm.Meta{},
Locations: []any{},
}
if opts.SSLCertificateID > 0 {
config.CertificateID = opts.SSLCertificateID
config.SSLForced = true
config.HSTSEnabled = true
}
if found {
host, err := p.client.UpdateProxyHost(ctx, existing.ID, config)
if err != nil {
return "", fmt.Errorf("update proxy host: %w", err)
}
return strconv.Itoa(host.ID), nil
}
host, err := p.client.CreateProxyHost(ctx, config)
if err != nil {
return "", fmt.Errorf("create proxy host: %w", err)
}
return strconv.Itoa(host.ID), nil
}
func (p *NpmProvider) DeleteRoute(ctx context.Context, routeID string) error {
if routeID == "" {
return nil
}
id, err := strconv.Atoi(routeID)
if err != nil {
return fmt.Errorf("invalid npm proxy host id %q: %w", routeID, err)
}
if err := p.auth(ctx); err != nil {
return err
}
return p.client.DeleteProxyHost(ctx, id)
}
func (p *NpmProvider) ContainerLabels(_ string, _ int) map[string]string {
// NPM configures routing via its API, not Docker labels.
return nil
}
func (p *NpmProvider) Ping(ctx context.Context) error {
return p.client.Ping(ctx)
}
// auth authenticates to NPM if credentials are available.
func (p *NpmProvider) auth(ctx context.Context) error {
if p.email == "" {
return fmt.Errorf("NPM credentials not configured")
}
return p.client.Authenticate(ctx, p.email, p.password)
}