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:
+25
-4
@@ -91,10 +91,31 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize NPM client.
|
// Initialize NPM client (used for NPM-specific endpoints like certificates).
|
||||||
npmURL := envOrDefault("NPM_URL", settings.NpmURL)
|
npmURL := envOrDefault("NPM_URL", settings.NpmURL)
|
||||||
npmClient := npm.New(npmURL)
|
npmClient := npm.New(npmURL)
|
||||||
|
|
||||||
|
// Build proxy provider based on settings.
|
||||||
|
var proxyProvider proxy.Provider
|
||||||
|
switch settings.ProxyProvider {
|
||||||
|
case "none":
|
||||||
|
proxyProvider = proxy.NewNoneProvider()
|
||||||
|
slog.Info("proxy provider: none")
|
||||||
|
default:
|
||||||
|
// Default to NPM for backward compatibility (including "npm" and empty string).
|
||||||
|
npmPassword := ""
|
||||||
|
if settings.NpmPassword != "" {
|
||||||
|
decrypted, err := crypto.Decrypt(encKey, settings.NpmPassword)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("failed to decrypt NPM password for proxy provider", "error", err)
|
||||||
|
} else {
|
||||||
|
npmPassword = decrypted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxyProvider = proxy.NewNpmProvider(npmClient, settings.NpmEmail, npmPassword)
|
||||||
|
slog.Info("proxy provider: npm", "url", npmURL)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize services.
|
// Initialize services.
|
||||||
healthChecker := health.New()
|
healthChecker := health.New()
|
||||||
notifier := notify.New()
|
notifier := notify.New()
|
||||||
@@ -115,7 +136,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
defer stopLogger()
|
defer stopLogger()
|
||||||
|
|
||||||
dep := deployer.New(dockerClient, npmClient, db, healthChecker, notifier, eventBus, encKey)
|
dep := deployer.New(dockerClient, proxyProvider, db, healthChecker, notifier, eventBus, encKey)
|
||||||
|
|
||||||
// Initialize webhook handler.
|
// Initialize webhook handler.
|
||||||
webhookHandler := webhook.NewHandler(db, dep, dockerClient)
|
webhookHandler := webhook.NewHandler(db, dep, dockerClient)
|
||||||
@@ -144,7 +165,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize proxy manager and health monitor.
|
// Initialize proxy manager and health monitor.
|
||||||
proxyManager := proxy.NewManager(db, npmClient)
|
proxyManager := proxy.NewManager(db, proxyProvider)
|
||||||
proxyHealth := proxy.NewHealthMonitor(db, eventBus)
|
proxyHealth := proxy.NewHealthMonitor(db, eventBus)
|
||||||
if err := proxyHealth.Start("5m"); err != nil {
|
if err := proxyHealth.Start("5m"); err != nil {
|
||||||
slog.Warn("failed to start proxy health monitor", "error", err)
|
slog.Warn("failed to start proxy health monitor", "error", err)
|
||||||
@@ -260,7 +281,7 @@ func main() {
|
|||||||
scheduleAutobackup(settings.BackupEnabled, settings.BackupIntervalHours)
|
scheduleAutobackup(settings.BackupEnabled, settings.BackupIntervalHours)
|
||||||
|
|
||||||
// Build API server.
|
// Build API server.
|
||||||
apiServer := api.NewServer(db, dockerClient, npmClient, dep, webhookHandler, eventBus, encKey)
|
apiServer := api.NewServer(db, dockerClient, npmClient, proxyProvider, dep, webhookHandler, eventBus, encKey)
|
||||||
apiServer.SetStaleScanner(staleScanner)
|
apiServer.SetStaleScanner(staleScanner)
|
||||||
apiServer.SetProxyManager(proxyManager)
|
apiServer.SetProxyManager(proxyManager)
|
||||||
apiServer.SetBackupEngine(backupEngine)
|
apiServer.SetBackupEngine(backupEngine)
|
||||||
|
|||||||
@@ -38,12 +38,13 @@ func (s *Server) getHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
result["docker"] = map[string]any{"connected": true}
|
result["docker"] = map[string]any{"connected": true}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check NPM connectivity if configured.
|
// Check proxy provider connectivity.
|
||||||
if s.npm != nil {
|
if s.proxyProvider != nil {
|
||||||
if err := s.npm.Ping(ctx); err != nil {
|
providerName := s.proxyProvider.Name()
|
||||||
result["npm"] = map[string]any{"connected": false, "error": "NPM unreachable"}
|
if err := s.proxyProvider.Ping(ctx); err != nil {
|
||||||
|
result["proxy"] = map[string]any{"provider": providerName, "connected": false, "error": providerName + " unreachable"}
|
||||||
} else {
|
} else {
|
||||||
result["npm"] = map[string]any{"connected": true}
|
result["proxy"] = map[string]any{"provider": providerName, "connected": true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"github.com/alexei/docker-watcher/internal/crypto"
|
|
||||||
"github.com/alexei/docker-watcher/internal/store"
|
"github.com/alexei/docker-watcher/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,18 +115,10 @@ func (s *Server) removeInstance(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete NPM proxy host if it has one.
|
// Delete proxy route if it has one.
|
||||||
if inst.NpmProxyID > 0 {
|
if inst.ProxyRouteID != "" {
|
||||||
settings, err := s.store.GetSettings()
|
if err := s.proxyProvider.DeleteRoute(r.Context(), inst.ProxyRouteID); err != nil {
|
||||||
if err == nil {
|
slog.Warn("delete proxy route on instance removal", "route_id", inst.ProxyRouteID, "error", err)
|
||||||
npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
|
|
||||||
if err == nil {
|
|
||||||
if authErr := s.npm.Authenticate(r.Context(), settings.NpmEmail, npmPassword); authErr == nil {
|
|
||||||
if delErr := s.npm.DeleteProxyHost(r.Context(), inst.NpmProxyID); delErr != nil {
|
|
||||||
slog.Warn("delete proxy host on instance removal", "proxy_id", inst.NpmProxyID, "error", delErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-12
@@ -26,10 +26,11 @@ type DNSProviderChangedFunc func(provider dns.Provider)
|
|||||||
|
|
||||||
// Server holds all dependencies for the API layer.
|
// Server holds all dependencies for the API layer.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
store *store.Store
|
store *store.Store
|
||||||
docker *docker.Client
|
docker *docker.Client
|
||||||
npm *npm.Client
|
npm *npm.Client // optional: only for NPM-specific endpoints (certificates)
|
||||||
deployer DeployTriggerer
|
proxyProvider proxy.Provider
|
||||||
|
deployer DeployTriggerer
|
||||||
webhook *webhook.Handler
|
webhook *webhook.Handler
|
||||||
eventBus *events.Bus
|
eventBus *events.Bus
|
||||||
encKey [32]byte
|
encKey [32]byte
|
||||||
@@ -53,6 +54,7 @@ func NewServer(
|
|||||||
st *store.Store,
|
st *store.Store,
|
||||||
dockerClient *docker.Client,
|
dockerClient *docker.Client,
|
||||||
npmClient *npm.Client,
|
npmClient *npm.Client,
|
||||||
|
proxyProvider proxy.Provider,
|
||||||
deployer DeployTriggerer,
|
deployer DeployTriggerer,
|
||||||
webhookHandler *webhook.Handler,
|
webhookHandler *webhook.Handler,
|
||||||
eventBus *events.Bus,
|
eventBus *events.Bus,
|
||||||
@@ -61,14 +63,15 @@ func NewServer(
|
|||||||
localAuth := auth.NewLocalAuth(encKey)
|
localAuth := auth.NewLocalAuth(encKey)
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
store: st,
|
store: st,
|
||||||
docker: dockerClient,
|
docker: dockerClient,
|
||||||
npm: npmClient,
|
npm: npmClient,
|
||||||
deployer: deployer,
|
proxyProvider: proxyProvider,
|
||||||
webhook: webhookHandler,
|
deployer: deployer,
|
||||||
eventBus: eventBus,
|
webhook: webhookHandler,
|
||||||
encKey: encKey,
|
eventBus: eventBus,
|
||||||
localAuth: localAuth,
|
encKey: encKey,
|
||||||
|
localAuth: localAuth,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to initialize OIDC provider from stored settings.
|
// Try to initialize OIDC provider from stored settings.
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type settingsRequest struct {
|
|||||||
DNSProvider *string `json:"dns_provider,omitempty"`
|
DNSProvider *string `json:"dns_provider,omitempty"`
|
||||||
CloudflareAPIToken string `json:"cloudflare_api_token"`
|
CloudflareAPIToken string `json:"cloudflare_api_token"`
|
||||||
CloudflareZoneID *string `json:"cloudflare_zone_id,omitempty"`
|
CloudflareZoneID *string `json:"cloudflare_zone_id,omitempty"`
|
||||||
|
ProxyProvider *string `json:"proxy_provider,omitempty"`
|
||||||
BackupEnabled *bool `json:"backup_enabled,omitempty"`
|
BackupEnabled *bool `json:"backup_enabled,omitempty"`
|
||||||
BackupIntervalHours *int `json:"backup_interval_hours,omitempty"`
|
BackupIntervalHours *int `json:"backup_interval_hours,omitempty"`
|
||||||
BackupRetentionCount *int `json:"backup_retention_count,omitempty"`
|
BackupRetentionCount *int `json:"backup_retention_count,omitempty"`
|
||||||
@@ -65,6 +66,7 @@ func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
"dns_provider": settings.DNSProvider,
|
"dns_provider": settings.DNSProvider,
|
||||||
"has_cloudflare_api_token": settings.CloudflareAPIToken != "",
|
"has_cloudflare_api_token": settings.CloudflareAPIToken != "",
|
||||||
"cloudflare_zone_id": settings.CloudflareZoneID,
|
"cloudflare_zone_id": settings.CloudflareZoneID,
|
||||||
|
"proxy_provider": settings.ProxyProvider,
|
||||||
"backup_enabled": settings.BackupEnabled,
|
"backup_enabled": settings.BackupEnabled,
|
||||||
"backup_interval_hours": settings.BackupIntervalHours,
|
"backup_interval_hours": settings.BackupIntervalHours,
|
||||||
"backup_retention_count": settings.BackupRetentionCount,
|
"backup_retention_count": settings.BackupRetentionCount,
|
||||||
@@ -166,6 +168,16 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
updated.CloudflareZoneID = *req.CloudflareZoneID
|
updated.CloudflareZoneID = *req.CloudflareZoneID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proxy provider setting.
|
||||||
|
if req.ProxyProvider != nil {
|
||||||
|
prov := *req.ProxyProvider
|
||||||
|
if prov != "" && prov != "none" && prov != "npm" {
|
||||||
|
respondError(w, http.StatusBadRequest, "proxy_provider must be 'none' or 'npm'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updated.ProxyProvider = prov
|
||||||
|
}
|
||||||
|
|
||||||
// Backup settings.
|
// Backup settings.
|
||||||
if req.BackupEnabled != nil {
|
if req.BackupEnabled != nil {
|
||||||
updated.BackupEnabled = *req.BackupEnabled
|
updated.BackupEnabled = *req.BackupEnabled
|
||||||
|
|||||||
+4
-13
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"github.com/alexei/docker-watcher/internal/crypto"
|
|
||||||
"github.com/alexei/docker-watcher/internal/events"
|
"github.com/alexei/docker-watcher/internal/events"
|
||||||
"github.com/alexei/docker-watcher/internal/stale"
|
"github.com/alexei/docker-watcher/internal/stale"
|
||||||
"github.com/alexei/docker-watcher/internal/store"
|
"github.com/alexei/docker-watcher/internal/store"
|
||||||
@@ -121,18 +120,10 @@ func (s *Server) cleanupInstance(r *http.Request, inst store.Instance) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete NPM proxy host if present.
|
// Delete proxy route if present.
|
||||||
if inst.NpmProxyID > 0 {
|
if inst.ProxyRouteID != "" {
|
||||||
settings, err := s.store.GetSettings()
|
if err := s.proxyProvider.DeleteRoute(ctx, inst.ProxyRouteID); err != nil {
|
||||||
if err == nil {
|
slog.Warn("stale cleanup: delete proxy route", "route_id", inst.ProxyRouteID, "error", err)
|
||||||
npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
|
|
||||||
if err == nil {
|
|
||||||
if authErr := s.npm.Authenticate(ctx, settings.NpmEmail, npmPassword); authErr == nil {
|
|
||||||
if delErr := s.npm.DeleteProxyHost(ctx, inst.NpmProxyID); delErr != nil {
|
|
||||||
slog.Warn("stale cleanup: delete proxy host", "proxy_id", inst.NpmProxyID, "error", delErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ func (d *Deployer) blueGreenDeploy(
|
|||||||
settings store.Settings,
|
settings store.Settings,
|
||||||
deployID string,
|
deployID string,
|
||||||
imageTag string,
|
imageTag string,
|
||||||
) (string, int, string, error) {
|
) (string, string, string, error) {
|
||||||
// Find existing running instance for this stage (the "blue" instance).
|
// Find existing running instance for this stage (the "blue" instance).
|
||||||
existingInstances, err := d.store.GetInstancesByStageID(stage.ID)
|
existingInstances, err := d.store.GetInstancesByStageID(stage.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, "", fmt.Errorf("get existing instances: %w", err)
|
return "", "", "", fmt.Errorf("get existing instances: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var blueInstance *store.Instance
|
var blueInstance *store.Instance
|
||||||
@@ -49,18 +49,18 @@ func (d *Deployer) blueGreenDeploy(
|
|||||||
|
|
||||||
authConfig, err := d.buildRegistryAuth(project)
|
authConfig, err := d.buildRegistryAuth(project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, "", fmt.Errorf("build registry auth: %w", err)
|
return "", "", "", fmt.Errorf("build registry auth: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.docker.PullImage(ctx, project.Image, imageTag, authConfig); err != nil {
|
if err := d.docker.PullImage(ctx, project.Image, imageTag, authConfig); err != nil {
|
||||||
return "", 0, "", fmt.Errorf("pull image: %w", err)
|
return "", "", "", fmt.Errorf("pull image: %w", err)
|
||||||
}
|
}
|
||||||
d.logDeploy(deployID, "Image pulled successfully", "info")
|
d.logDeploy(deployID, "Image pulled successfully", "info")
|
||||||
|
|
||||||
// Step 2: Ensure network.
|
// Step 2: Ensure network.
|
||||||
networkID, err := d.docker.EnsureNetwork(ctx, settings.Network)
|
networkID, err := d.docker.EnsureNetwork(ctx, settings.Network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, "", fmt.Errorf("ensure network: %w", err)
|
return "", "", "", fmt.Errorf("ensure network: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Create and start green container.
|
// Step 3: Create and start green container.
|
||||||
@@ -92,7 +92,7 @@ func (d *Deployer) blueGreenDeploy(
|
|||||||
d.logDeploy(deployID, fmt.Sprintf("Blue-green: creating green container %s", containerName), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Blue-green: creating green container %s", containerName), "info")
|
||||||
containerID, err := d.docker.CreateContainer(ctx, containerCfg)
|
containerID, err := d.docker.CreateContainer(ctx, containerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, instanceID, fmt.Errorf("create container: %w", err)
|
return "", "", instanceID, fmt.Errorf("create container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create instance record.
|
// Create instance record.
|
||||||
@@ -107,7 +107,7 @@ func (d *Deployer) blueGreenDeploy(
|
|||||||
Port: project.Port,
|
Port: project.Port,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return containerID, 0, instanceID, fmt.Errorf("create instance record: %w", err)
|
return containerID, "", instanceID, fmt.Errorf("create instance record: %w", err)
|
||||||
}
|
}
|
||||||
instanceID = inst.ID
|
instanceID = inst.ID
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ func (d *Deployer) blueGreenDeploy(
|
|||||||
|
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Blue-green: starting green container %s", containerName), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Blue-green: starting green container %s", containerName), "info")
|
||||||
if err := d.docker.StartContainer(ctx, containerID); err != nil {
|
if err := d.docker.StartContainer(ctx, containerID); err != nil {
|
||||||
return containerID, 0, instanceID, fmt.Errorf("start container: %w", err)
|
return containerID, "", instanceID, fmt.Errorf("start container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.store.UpdateInstanceStatus(instanceID, "running"); err != nil {
|
if err := d.store.UpdateInstanceStatus(instanceID, "running"); err != nil {
|
||||||
@@ -136,25 +136,25 @@ func (d *Deployer) blueGreenDeploy(
|
|||||||
d.logDeploy(deployID, fmt.Sprintf("Blue-green: health checking green at %s", healthURL), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Blue-green: health checking green at %s", healthURL), "info")
|
||||||
|
|
||||||
if err := d.health.Check(ctx, healthURL); err != nil {
|
if err := d.health.Check(ctx, healthURL); err != nil {
|
||||||
return containerID, 0, instanceID, fmt.Errorf("health check green: %w", err)
|
return containerID, "", instanceID, fmt.Errorf("health check green: %w", err)
|
||||||
}
|
}
|
||||||
d.logDeploy(deployID, "Blue-green: green health check passed", "info")
|
d.logDeploy(deployID, "Blue-green: green health check passed", "info")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Swap NPM proxy to green.
|
// Step 5: Swap proxy to green.
|
||||||
var npmProxyID int
|
var proxyRouteID string
|
||||||
if stage.EnableProxy {
|
if stage.EnableProxy {
|
||||||
if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil {
|
if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil {
|
||||||
slog.Warn("update deploy status", "error", err)
|
slog.Warn("update deploy status", "error", err)
|
||||||
}
|
}
|
||||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
||||||
|
|
||||||
npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
proxyRouteID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return containerID, 0, instanceID, fmt.Errorf("configure proxy: %w", err)
|
return containerID, "", instanceID, fmt.Errorf("configure proxy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inst.NpmProxyID = npmProxyID
|
inst.ProxyRouteID = proxyRouteID
|
||||||
d.logDeploy(deployID, "Blue-green: proxy swapped to green container", "info")
|
d.logDeploy(deployID, "Blue-green: proxy swapped to green container", "info")
|
||||||
|
|
||||||
// Create/update DNS record for the green instance.
|
// Create/update DNS record for the green instance.
|
||||||
@@ -180,5 +180,5 @@ func (d *Deployer) blueGreenDeploy(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return containerID, npmProxyID, instanceID, nil
|
return containerID, proxyRouteID, instanceID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/alexei/docker-watcher/internal/events"
|
"github.com/alexei/docker-watcher/internal/events"
|
||||||
"github.com/alexei/docker-watcher/internal/health"
|
"github.com/alexei/docker-watcher/internal/health"
|
||||||
"github.com/alexei/docker-watcher/internal/notify"
|
"github.com/alexei/docker-watcher/internal/notify"
|
||||||
"github.com/alexei/docker-watcher/internal/npm"
|
"github.com/alexei/docker-watcher/internal/proxy"
|
||||||
"github.com/alexei/docker-watcher/internal/store"
|
"github.com/alexei/docker-watcher/internal/store"
|
||||||
"github.com/alexei/docker-watcher/internal/volume"
|
"github.com/alexei/docker-watcher/internal/volume"
|
||||||
"github.com/moby/moby/api/types/mount"
|
"github.com/moby/moby/api/types/mount"
|
||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
// It implements both webhook.DeployTriggerer and registry.DeployTriggerer.
|
// It implements both webhook.DeployTriggerer and registry.DeployTriggerer.
|
||||||
type Deployer struct {
|
type Deployer struct {
|
||||||
docker *docker.Client
|
docker *docker.Client
|
||||||
npm *npm.Client
|
proxy proxy.Provider
|
||||||
store *store.Store
|
store *store.Store
|
||||||
health *health.Checker
|
health *health.Checker
|
||||||
notifier *notify.Notifier
|
notifier *notify.Notifier
|
||||||
@@ -49,7 +49,7 @@ type EventPublisher interface {
|
|||||||
// New creates a new Deployer with all required dependencies.
|
// New creates a new Deployer with all required dependencies.
|
||||||
func New(
|
func New(
|
||||||
dockerClient *docker.Client,
|
dockerClient *docker.Client,
|
||||||
npmClient *npm.Client,
|
proxyProvider proxy.Provider,
|
||||||
st *store.Store,
|
st *store.Store,
|
||||||
checker *health.Checker,
|
checker *health.Checker,
|
||||||
notifier *notify.Notifier,
|
notifier *notify.Notifier,
|
||||||
@@ -58,7 +58,7 @@ func New(
|
|||||||
) *Deployer {
|
) *Deployer {
|
||||||
return &Deployer{
|
return &Deployer{
|
||||||
docker: dockerClient,
|
docker: dockerClient,
|
||||||
npm: npmClient,
|
proxy: proxyProvider,
|
||||||
store: st,
|
store: st,
|
||||||
health: checker,
|
health: checker,
|
||||||
notifier: notifier,
|
notifier: notifier,
|
||||||
@@ -161,20 +161,20 @@ func (d *Deployer) runDeploy(ctx context.Context, project store.Project, stage s
|
|||||||
}
|
}
|
||||||
|
|
||||||
var containerID string
|
var containerID string
|
||||||
var npmProxyID int
|
var proxyRouteID string
|
||||||
var instanceID string
|
var instanceID string
|
||||||
var deployErr error
|
var deployErr error
|
||||||
|
|
||||||
if stage.MaxInstances == 1 {
|
if stage.MaxInstances == 1 {
|
||||||
containerID, npmProxyID, instanceID, deployErr = d.blueGreenDeploy(ctx, project, stage, settings, deployID, imageTag)
|
containerID, proxyRouteID, instanceID, deployErr = d.blueGreenDeploy(ctx, project, stage, settings, deployID, imageTag)
|
||||||
} else {
|
} else {
|
||||||
containerID, npmProxyID, instanceID, deployErr = d.executeDeploy(ctx, project, stage, settings, deployID, imageTag)
|
containerID, proxyRouteID, instanceID, deployErr = d.executeDeploy(ctx, project, stage, settings, deployID, imageTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
if deployErr != nil {
|
if deployErr != nil {
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Deploy failed: %v", deployErr), "error")
|
d.logDeploy(deployID, fmt.Sprintf("Deploy failed: %v", deployErr), "error")
|
||||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "failed", deployErr.Error())
|
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "failed", deployErr.Error())
|
||||||
d.rollback(ctx, deployID, containerID, npmProxyID, instanceID)
|
d.rollback(ctx, deployID, containerID, proxyRouteID, instanceID)
|
||||||
|
|
||||||
d.notifier.Send(settings.NotificationURL, notify.Event{
|
d.notifier.Send(settings.NotificationURL, notify.Event{
|
||||||
Type: "deploy_failure",
|
Type: "deploy_failure",
|
||||||
@@ -250,7 +250,7 @@ func (d *Deployer) TriggerDeploy(ctx context.Context, projectID, stageID, imageT
|
|||||||
}
|
}
|
||||||
|
|
||||||
// executeDeploy runs the deploy pipeline steps and returns rollback-relevant state.
|
// executeDeploy runs the deploy pipeline steps and returns rollback-relevant state.
|
||||||
// It returns (containerID, npmProxyID, instanceID, error).
|
// It returns (containerID, proxyRouteID, instanceID, error).
|
||||||
func (d *Deployer) executeDeploy(
|
func (d *Deployer) executeDeploy(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
project store.Project,
|
project store.Project,
|
||||||
@@ -258,9 +258,9 @@ func (d *Deployer) executeDeploy(
|
|||||||
settings store.Settings,
|
settings store.Settings,
|
||||||
deployID string,
|
deployID string,
|
||||||
imageTag string,
|
imageTag string,
|
||||||
) (string, int, string, error) {
|
) (string, string, string, error) {
|
||||||
var containerID string
|
var containerID string
|
||||||
var npmProxyID int
|
var proxyRouteID string
|
||||||
var instanceID string
|
var instanceID string
|
||||||
|
|
||||||
// Step 1: Pull image.
|
// Step 1: Pull image.
|
||||||
@@ -272,18 +272,18 @@ func (d *Deployer) executeDeploy(
|
|||||||
|
|
||||||
authConfig, err := d.buildRegistryAuth(project)
|
authConfig, err := d.buildRegistryAuth(project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return containerID, npmProxyID, instanceID, fmt.Errorf("build registry auth: %w", err)
|
return containerID, proxyRouteID, instanceID, fmt.Errorf("build registry auth: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.docker.PullImage(ctx, project.Image, imageTag, authConfig); err != nil {
|
if err := d.docker.PullImage(ctx, project.Image, imageTag, authConfig); err != nil {
|
||||||
return containerID, npmProxyID, instanceID, fmt.Errorf("pull image: %w", err)
|
return containerID, proxyRouteID, instanceID, fmt.Errorf("pull image: %w", err)
|
||||||
}
|
}
|
||||||
d.logDeploy(deployID, "Image pulled successfully", "info")
|
d.logDeploy(deployID, "Image pulled successfully", "info")
|
||||||
|
|
||||||
// Step 2: Ensure network exists.
|
// Step 2: Ensure network exists.
|
||||||
networkID, err := d.docker.EnsureNetwork(ctx, settings.Network)
|
networkID, err := d.docker.EnsureNetwork(ctx, settings.Network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return containerID, npmProxyID, instanceID, fmt.Errorf("ensure network: %w", err)
|
return containerID, proxyRouteID, instanceID, fmt.Errorf("ensure network: %w", err)
|
||||||
}
|
}
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Network %s ready (ID: %s)", settings.Network, truncateID(networkID)), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Network %s ready (ID: %s)", settings.Network, truncateID(networkID)), "info")
|
||||||
|
|
||||||
@@ -318,7 +318,7 @@ func (d *Deployer) executeDeploy(
|
|||||||
d.logDeploy(deployID, fmt.Sprintf("Creating container %s", containerName), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Creating container %s", containerName), "info")
|
||||||
containerID, err = d.docker.CreateContainer(ctx, containerCfg)
|
containerID, err = d.docker.CreateContainer(ctx, containerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return containerID, npmProxyID, instanceID, fmt.Errorf("create container: %w", err)
|
return containerID, proxyRouteID, instanceID, fmt.Errorf("create container: %w", err)
|
||||||
}
|
}
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Container created (ID: %s)", truncateID(containerID)), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Container created (ID: %s)", truncateID(containerID)), "info")
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@ func (d *Deployer) executeDeploy(
|
|||||||
Port: project.Port,
|
Port: project.Port,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return containerID, npmProxyID, instanceID, fmt.Errorf("create instance record: %w", err)
|
return containerID, proxyRouteID, instanceID, fmt.Errorf("create instance record: %w", err)
|
||||||
}
|
}
|
||||||
instanceID = inst.ID
|
instanceID = inst.ID
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ func (d *Deployer) executeDeploy(
|
|||||||
|
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Starting container %s", containerName), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Starting container %s", containerName), "info")
|
||||||
if err := d.docker.StartContainer(ctx, containerID); err != nil {
|
if err := d.docker.StartContainer(ctx, containerID); err != nil {
|
||||||
return containerID, npmProxyID, instanceID, fmt.Errorf("start container: %w", err)
|
return containerID, proxyRouteID, instanceID, fmt.Errorf("start container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.store.UpdateInstanceStatus(instanceID, "running"); err != nil {
|
if err := d.store.UpdateInstanceStatus(instanceID, "running"); err != nil {
|
||||||
@@ -364,13 +364,13 @@ func (d *Deployer) executeDeploy(
|
|||||||
}
|
}
|
||||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
||||||
|
|
||||||
npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
proxyRouteID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return containerID, npmProxyID, instanceID, fmt.Errorf("configure proxy: %w", err)
|
return containerID, proxyRouteID, instanceID, fmt.Errorf("configure proxy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update instance with NPM proxy ID.
|
// Update instance with proxy route ID.
|
||||||
inst.NpmProxyID = npmProxyID
|
inst.ProxyRouteID = proxyRouteID
|
||||||
inst.Subdomain = subdomain
|
inst.Subdomain = subdomain
|
||||||
if err := d.store.UpdateInstance(inst); err != nil {
|
if err := d.store.UpdateInstance(inst); err != nil {
|
||||||
slog.Warn("update instance with proxy ID", "error", err)
|
slog.Warn("update instance with proxy ID", "error", err)
|
||||||
@@ -398,19 +398,19 @@ func (d *Deployer) executeDeploy(
|
|||||||
d.logDeploy(deployID, fmt.Sprintf("Running health check: %s", healthURL), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Running health check: %s", healthURL), "info")
|
||||||
|
|
||||||
if err := d.health.Check(ctx, healthURL); err != nil {
|
if err := d.health.Check(ctx, healthURL); err != nil {
|
||||||
return containerID, npmProxyID, instanceID, fmt.Errorf("health check: %w", err)
|
return containerID, proxyRouteID, instanceID, fmt.Errorf("health check: %w", err)
|
||||||
}
|
}
|
||||||
d.logDeploy(deployID, "Health check passed", "info")
|
d.logDeploy(deployID, "Health check passed", "info")
|
||||||
} else {
|
} else {
|
||||||
d.logDeploy(deployID, "No health check configured, skipping", "info")
|
d.logDeploy(deployID, "No health check configured, skipping", "info")
|
||||||
}
|
}
|
||||||
|
|
||||||
return containerID, npmProxyID, instanceID, nil
|
return containerID, proxyRouteID, instanceID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// configureProxy creates or updates an NPM proxy host for the deployed container.
|
// configureProxy creates or updates a proxy route for the deployed container.
|
||||||
// It authenticates to NPM using credentials from settings, then creates the proxy.
|
// Uses the configured proxy.Provider (NPM, Traefik, or None).
|
||||||
// Returns the NPM proxy host ID.
|
// Returns the proxy route ID string.
|
||||||
func (d *Deployer) configureProxy(
|
func (d *Deployer) configureProxy(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
deployID string,
|
deployID string,
|
||||||
@@ -418,64 +418,21 @@ func (d *Deployer) configureProxy(
|
|||||||
containerName string,
|
containerName string,
|
||||||
containerPort int,
|
containerPort int,
|
||||||
subdomain string,
|
subdomain string,
|
||||||
) (int, error) {
|
) (string, error) {
|
||||||
// Authenticate to NPM.
|
|
||||||
npmPassword, err := d.decryptNpmPassword(settings.NpmPassword)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("decrypt npm password: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := d.npm.Authenticate(ctx, settings.NpmEmail, npmPassword); err != nil {
|
|
||||||
return 0, fmt.Errorf("authenticate to npm: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fqdn := subdomain + "." + settings.Domain
|
fqdn := subdomain + "." + settings.Domain
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Configuring proxy: %s -> %s:%d", fqdn, containerName, containerPort), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Configuring proxy (%s): %s -> %s:%d", d.proxy.Name(), fqdn, containerName, containerPort), "info")
|
||||||
|
|
||||||
// Check if a proxy host already exists for this domain.
|
routeID, err := d.proxy.ConfigureRoute(ctx, fqdn, containerName, containerPort, proxy.RouteOptions{
|
||||||
existing, found, err := d.npm.FindProxyHostByDomain(ctx, fqdn)
|
SSLCertificateID: settings.SSLCertificateID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("find existing proxy host: %w", err)
|
return "", fmt.Errorf("configure proxy route: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyConfig := npm.ProxyHostConfig{
|
if routeID != "" {
|
||||||
DomainNames: []string{fqdn},
|
d.logDeploy(deployID, fmt.Sprintf("Proxy route configured (ID: %s)", routeID), "info")
|
||||||
ForwardScheme: "http",
|
|
||||||
ForwardHost: containerName,
|
|
||||||
ForwardPort: containerPort,
|
|
||||||
BlockExploits: true,
|
|
||||||
AllowWebsocket: true,
|
|
||||||
HTTP2Support: true,
|
|
||||||
Meta: npm.Meta{},
|
|
||||||
Locations: []any{},
|
|
||||||
}
|
}
|
||||||
|
return routeID, nil
|
||||||
// Apply SSL certificate if configured in settings.
|
|
||||||
if settings.SSLCertificateID > 0 {
|
|
||||||
proxyConfig.CertificateID = settings.SSLCertificateID
|
|
||||||
proxyConfig.SSLForced = true
|
|
||||||
proxyConfig.HSTSEnabled = true
|
|
||||||
proxyConfig.HTTP2Support = true
|
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Using SSL certificate ID %d", settings.SSLCertificateID), "info")
|
|
||||||
}
|
|
||||||
|
|
||||||
if found {
|
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Updating existing proxy host %d for %s", existing.ID, fqdn), "info")
|
|
||||||
host, err := d.npm.UpdateProxyHost(ctx, existing.ID, proxyConfig)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("update proxy host: %w", err)
|
|
||||||
}
|
|
||||||
d.logDeploy(deployID, "Proxy host updated", "info")
|
|
||||||
return host.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Creating new proxy host for %s", fqdn), "info")
|
|
||||||
host, err := d.npm.CreateProxyHost(ctx, proxyConfig)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("create proxy host: %w", err)
|
|
||||||
}
|
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Proxy host created (ID: %d)", host.ID), "info")
|
|
||||||
return host.ID, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// enforceMaxInstances removes the oldest instances when the stage has reached its limit.
|
// enforceMaxInstances removes the oldest instances when the stage has reached its limit.
|
||||||
@@ -538,15 +495,10 @@ func (d *Deployer) removeInstance(ctx context.Context, inst store.Instance, sett
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete NPM proxy host.
|
// Delete proxy route.
|
||||||
if inst.NpmProxyID > 0 {
|
if inst.ProxyRouteID != "" {
|
||||||
npmPassword, err := d.decryptNpmPassword(settings.NpmPassword)
|
if err := d.proxy.DeleteRoute(ctx, inst.ProxyRouteID); err != nil {
|
||||||
if err != nil {
|
slog.Warn("delete proxy route", "route_id", inst.ProxyRouteID, "error", err)
|
||||||
slog.Warn("decrypt npm password for proxy cleanup", "error", err)
|
|
||||||
} else if authErr := d.npm.Authenticate(ctx, settings.NpmEmail, npmPassword); authErr != nil {
|
|
||||||
slog.Warn("authenticate npm for proxy cleanup", "error", authErr)
|
|
||||||
} else if delErr := d.npm.DeleteProxyHost(ctx, inst.NpmProxyID); delErr != nil {
|
|
||||||
slog.Warn("delete proxy host", "proxy_id", inst.NpmProxyID, "error", delErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove DNS record for this instance.
|
// Remove DNS record for this instance.
|
||||||
@@ -591,15 +543,6 @@ func (d *Deployer) buildRegistryAuth(project store.Project) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// decryptNpmPassword decrypts the NPM password from settings.
|
|
||||||
// Returns empty string if the encrypted password is empty.
|
|
||||||
func (d *Deployer) decryptNpmPassword(encryptedPassword string) (string, error) {
|
|
||||||
if encryptedPassword == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return crypto.Decrypt(d.encKey, encryptedPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeEnvVars builds the final environment variable list for a container:
|
// mergeEnvVars builds the final environment variable list for a container:
|
||||||
// 1. Parse project-level env JSON
|
// 1. Parse project-level env JSON
|
||||||
// 2. Overlay with stage-level env overrides (stage wins on key conflict)
|
// 2. Overlay with stage-level env overrides (stage wins on key conflict)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// rollback cleans up a failed deployment by removing the container,
|
// rollback cleans up a failed deployment by removing the container,
|
||||||
// deleting the NPM proxy host, and updating the instance status.
|
// deleting the proxy route, and updating the instance status.
|
||||||
// Errors during rollback are logged but do not prevent other cleanup steps.
|
// Errors during rollback are logged but do not prevent other cleanup steps.
|
||||||
func (d *Deployer) rollback(ctx context.Context, deployID string, containerID string, npmProxyID int, instanceID string) {
|
func (d *Deployer) rollback(ctx context.Context, deployID string, containerID string, proxyRouteID string, instanceID string) {
|
||||||
d.logDeploy(deployID, "Rolling back failed deployment", "warn")
|
d.logDeploy(deployID, "Rolling back failed deployment", "warn")
|
||||||
|
|
||||||
// Remove the container if it was created.
|
// Remove the container if it was created.
|
||||||
@@ -22,23 +22,13 @@ func (d *Deployer) rollback(ctx context.Context, deployID string, containerID st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the NPM proxy host if it was created.
|
// Delete the proxy route if it was created.
|
||||||
if npmProxyID > 0 {
|
if proxyRouteID != "" {
|
||||||
settings, err := d.store.GetSettings()
|
if err := d.proxy.DeleteRoute(ctx, proxyRouteID); err != nil {
|
||||||
if err != nil {
|
slog.Warn("rollback: delete proxy route", "route_id", proxyRouteID, "error", err)
|
||||||
slog.Warn("rollback: get settings for npm auth", "error", err)
|
d.logDeploy(deployID, fmt.Sprintf("Rollback: failed to delete proxy route: %v", err), "error")
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Rollback: failed to get settings for proxy cleanup: %v", err), "error")
|
|
||||||
} else if npmPassword, err := d.decryptNpmPassword(settings.NpmPassword); err != nil {
|
|
||||||
slog.Warn("rollback: decrypt npm password", "error", err)
|
|
||||||
d.logDeploy(deployID, "Rollback: failed to decrypt NPM password for proxy cleanup", "error")
|
|
||||||
} else if err := d.npm.Authenticate(ctx, settings.NpmEmail, npmPassword); err != nil {
|
|
||||||
slog.Warn("rollback: authenticate npm", "error", err)
|
|
||||||
d.logDeploy(deployID, "Rollback: failed to authenticate NPM for proxy cleanup", "error")
|
|
||||||
} else if err := d.npm.DeleteProxyHost(ctx, npmProxyID); err != nil {
|
|
||||||
slog.Warn("rollback: delete proxy host", "proxy_id", npmProxyID, "error", err)
|
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Rollback: failed to delete proxy host: %v", err), "error")
|
|
||||||
} else {
|
} else {
|
||||||
d.logDeploy(deployID, "Rollback: proxy host deleted", "info")
|
d.logDeploy(deployID, "Rollback: proxy route deleted", "info")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-62
@@ -8,23 +8,22 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/alexei/docker-watcher/internal/dns"
|
"github.com/alexei/docker-watcher/internal/dns"
|
||||||
"github.com/alexei/docker-watcher/internal/npm"
|
|
||||||
"github.com/alexei/docker-watcher/internal/store"
|
"github.com/alexei/docker-watcher/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles the lifecycle of standalone proxy hosts.
|
// Manager handles the lifecycle of standalone proxy hosts.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
store *store.Store
|
store *store.Store
|
||||||
npm *npm.Client
|
provider Provider
|
||||||
dnsMu sync.RWMutex
|
dnsMu sync.RWMutex
|
||||||
dns dns.Provider // nil when wildcard DNS is active
|
dns dns.Provider // nil when wildcard DNS is active
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new proxy manager.
|
// 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{
|
return &Manager{
|
||||||
store: st,
|
store: st,
|
||||||
npm: npmClient,
|
provider: provider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ type ProxyView struct {
|
|||||||
CreatedAt string `json:"created_at"`
|
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) {
|
func (m *Manager) CreateProxy(ctx context.Context, req CreateProxyRequest) (store.StandaloneProxy, error) {
|
||||||
// Validate destination.
|
// Validate destination.
|
||||||
result := ValidateDestination(ctx, req.DestinationURL, req.DestinationPort)
|
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)
|
return store.StandaloneProxy{}, fmt.Errorf("get settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build NPM proxy host config.
|
// Create proxy route via provider.
|
||||||
config := npm.ProxyHostConfig{
|
routeID, err := m.provider.ConfigureRoute(ctx, req.Domain, req.DestinationURL, req.DestinationPort, RouteOptions{
|
||||||
DomainNames: []string{req.Domain},
|
SSLCertificateID: settings.SSLCertificateID,
|
||||||
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)
|
|
||||||
if err != nil {
|
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",
|
slog.Info("created proxy route for standalone proxy",
|
||||||
"domain", req.Domain, "npm_proxy_id", npmHost.ID)
|
"domain", req.Domain, "route_id", routeID, "provider", m.provider.Name())
|
||||||
|
|
||||||
// Save to store.
|
// Save to store.
|
||||||
proxy, err := m.store.CreateStandaloneProxy(store.StandaloneProxy{
|
proxy, err := m.store.CreateStandaloneProxy(store.StandaloneProxy{
|
||||||
@@ -114,14 +100,13 @@ func (m *Manager) CreateProxy(ctx context.Context, req CreateProxyRequest) (stor
|
|||||||
DestinationURL: req.DestinationURL,
|
DestinationURL: req.DestinationURL,
|
||||||
DestinationPort: req.DestinationPort,
|
DestinationPort: req.DestinationPort,
|
||||||
SSLCertificateID: settings.SSLCertificateID,
|
SSLCertificateID: settings.SSLCertificateID,
|
||||||
NpmProxyID: npmHost.ID,
|
|
||||||
HealthStatus: "unknown",
|
HealthStatus: "unknown",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Best effort: clean up the NPM host if store insert fails.
|
// Best effort: clean up the proxy route if store insert fails.
|
||||||
if delErr := m.npm.DeleteProxyHost(ctx, npmHost.ID); delErr != nil {
|
if delErr := m.provider.DeleteRoute(ctx, routeID); delErr != nil {
|
||||||
slog.Error("failed to clean up NPM proxy host after store error",
|
slog.Error("failed to clean up proxy route after store error",
|
||||||
"npm_proxy_id", npmHost.ID, "error", delErr)
|
"route_id", routeID, "error", delErr)
|
||||||
}
|
}
|
||||||
return store.StandaloneProxy{}, fmt.Errorf("save standalone proxy: %w", err)
|
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
|
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) {
|
func (m *Manager) UpdateProxy(ctx context.Context, id string, req UpdateProxyRequest) (store.StandaloneProxy, error) {
|
||||||
existing, err := m.store.GetStandaloneProxy(id)
|
existing, err := m.store.GetStandaloneProxy(id)
|
||||||
if err != nil {
|
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)
|
return store.StandaloneProxy{}, fmt.Errorf("get settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update NPM proxy host.
|
// Update proxy route via provider (ConfigureRoute handles create-or-update).
|
||||||
config := npm.ProxyHostConfig{
|
if _, err := m.provider.ConfigureRoute(ctx, req.Domain, req.DestinationURL, req.DestinationPort, RouteOptions{
|
||||||
DomainNames: []string{req.Domain},
|
SSLCertificateID: settings.SSLCertificateID,
|
||||||
ForwardScheme: "http",
|
}); err != nil {
|
||||||
ForwardHost: req.DestinationURL,
|
return store.StandaloneProxy{}, fmt.Errorf("update proxy route: %w", err)
|
||||||
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 store.
|
// Update store.
|
||||||
@@ -191,23 +164,24 @@ func (m *Manager) UpdateProxy(ctx context.Context, id string, req UpdateProxyReq
|
|||||||
return m.store.GetStandaloneProxy(id)
|
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 {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("get proxy: %w", err)
|
return fmt.Errorf("get proxy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete NPM proxy host.
|
// Delete proxy route via provider using the NpmProxyID as a string route ID.
|
||||||
if proxy.NpmProxyID > 0 {
|
if p.NpmProxyID > 0 {
|
||||||
if err := m.npm.DeleteProxyHost(ctx, proxy.NpmProxyID); err != nil {
|
routeID := fmt.Sprintf("%d", p.NpmProxyID)
|
||||||
slog.Warn("failed to delete NPM proxy host (continuing with store deletion)",
|
if err := m.provider.DeleteRoute(ctx, routeID); err != nil {
|
||||||
"npm_proxy_id", proxy.NpmProxyID, "error", err)
|
slog.Warn("failed to delete proxy route (continuing with store deletion)",
|
||||||
|
"route_id", routeID, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove DNS record.
|
// Remove DNS record.
|
||||||
m.removeDNS(ctx, proxy.Domain)
|
m.removeDNS(ctx, p.Domain)
|
||||||
|
|
||||||
if err := m.store.DeleteStandaloneProxy(id); err != nil {
|
if err := m.store.DeleteStandaloneProxy(id); err != nil {
|
||||||
return fmt.Errorf("delete standalone proxy: %w", err)
|
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()
|
instances, err := m.store.ListAllInstances()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list instances: %w", err)
|
return nil, fmt.Errorf("list instances: %w", err)
|
||||||
@@ -278,7 +252,7 @@ func (m *Manager) ListAllProxies() ([]ProxyView, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, inst := range instances {
|
for _, inst := range instances {
|
||||||
if inst.NpmProxyID <= 0 {
|
if inst.ProxyRouteID == "" && inst.NpmProxyID <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// NoneProvider is a no-op proxy provider for deployments that don't use a reverse proxy.
|
||||||
|
type NoneProvider struct{}
|
||||||
|
|
||||||
|
func NewNoneProvider() *NoneProvider { return &NoneProvider{} }
|
||||||
|
|
||||||
|
func (n *NoneProvider) Name() string { return "none" }
|
||||||
|
|
||||||
|
func (n *NoneProvider) ConfigureRoute(_ context.Context, _, _ string, _ int, _ RouteOptions) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoneProvider) DeleteRoute(_ context.Context, _ string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoneProvider) ContainerLabels(_ string, _ int) map[string]string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoneProvider) Ping(_ context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// RouteOptions holds optional configuration for a proxy route.
|
||||||
|
type RouteOptions struct {
|
||||||
|
SSLCertificateID int
|
||||||
|
ForwardScheme string // "http" or "https", defaults to "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the interface for proxy route management.
|
||||||
|
// Implementations handle the specifics of each proxy system (NPM, Traefik, etc.).
|
||||||
|
// The "None" provider implements all methods as no-ops.
|
||||||
|
type Provider interface {
|
||||||
|
// Name returns the provider identifier (e.g., "npm", "traefik", "none").
|
||||||
|
Name() string
|
||||||
|
|
||||||
|
// ConfigureRoute creates or updates a proxy route for the given domain.
|
||||||
|
// Returns a route ID string that can be used for updates and deletes.
|
||||||
|
// For NPM, the route ID is the proxy host ID (stringified).
|
||||||
|
// For Traefik, the route ID is the router name.
|
||||||
|
// For None, returns empty string.
|
||||||
|
ConfigureRoute(ctx context.Context, domain, targetHost string, targetPort int, opts RouteOptions) (routeID string, err error)
|
||||||
|
|
||||||
|
// DeleteRoute removes a proxy route by its route ID.
|
||||||
|
// Does nothing if routeID is empty.
|
||||||
|
DeleteRoute(ctx context.Context, routeID string) error
|
||||||
|
|
||||||
|
// ContainerLabels returns Docker labels to set on containers at creation time.
|
||||||
|
// Traefik uses labels for auto-discovery; NPM and None return nil.
|
||||||
|
ContainerLabels(domain string, port int) map[string]string
|
||||||
|
|
||||||
|
// Ping checks connectivity to the proxy backend.
|
||||||
|
// Returns nil if healthy or if the provider doesn't need external connectivity (None).
|
||||||
|
Ping(ctx context.Context) error
|
||||||
|
}
|
||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// instanceColumns is the canonical column list for instance queries.
|
// instanceColumns is the canonical column list for instance queries.
|
||||||
const instanceColumns = `id, stage_id, project_id, container_id, image_tag, subdomain, npm_proxy_id, status, port, last_alive_at, created_at, updated_at`
|
const instanceColumns = `id, stage_id, project_id, container_id, image_tag, subdomain, npm_proxy_id, proxy_route_id, status, port, last_alive_at, created_at, updated_at`
|
||||||
|
|
||||||
// scanInstance scans a row into an Instance struct using the canonical column order.
|
// scanInstance scans a row into an Instance struct using the canonical column order.
|
||||||
func scanInstance(scanner interface{ Scan(...any) error }) (Instance, error) {
|
func scanInstance(scanner interface{ Scan(...any) error }) (Instance, error) {
|
||||||
var inst Instance
|
var inst Instance
|
||||||
err := scanner.Scan(
|
err := scanner.Scan(
|
||||||
&inst.ID, &inst.StageID, &inst.ProjectID, &inst.ContainerID, &inst.ImageTag,
|
&inst.ID, &inst.StageID, &inst.ProjectID, &inst.ContainerID, &inst.ImageTag,
|
||||||
&inst.Subdomain, &inst.NpmProxyID, &inst.Status, &inst.Port,
|
&inst.Subdomain, &inst.NpmProxyID, &inst.ProxyRouteID, &inst.Status, &inst.Port,
|
||||||
&inst.LastAliveAt, &inst.CreatedAt, &inst.UpdatedAt,
|
&inst.LastAliveAt, &inst.CreatedAt, &inst.UpdatedAt,
|
||||||
)
|
)
|
||||||
return inst, err
|
return inst, err
|
||||||
@@ -30,9 +30,9 @@ func (s *Store) CreateInstance(inst Instance) (Instance, error) {
|
|||||||
|
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
`INSERT INTO instances (`+instanceColumns+`)
|
`INSERT INTO instances (`+instanceColumns+`)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
inst.ID, inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
inst.ID, inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
||||||
inst.Subdomain, inst.NpmProxyID, inst.Status, inst.Port,
|
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
||||||
inst.LastAliveAt, inst.CreatedAt, inst.UpdatedAt,
|
inst.LastAliveAt, inst.CreatedAt, inst.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -52,9 +52,9 @@ func (s *Store) CreateInstanceWithID(inst Instance) (Instance, error) {
|
|||||||
|
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
`INSERT INTO instances (`+instanceColumns+`)
|
`INSERT INTO instances (`+instanceColumns+`)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
inst.ID, inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
inst.ID, inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
||||||
inst.Subdomain, inst.NpmProxyID, inst.Status, inst.Port,
|
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
||||||
inst.LastAliveAt, inst.CreatedAt, inst.UpdatedAt,
|
inst.LastAliveAt, inst.CreatedAt, inst.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -123,10 +123,10 @@ func (s *Store) ListAllInstances() ([]Instance, error) {
|
|||||||
func (s *Store) UpdateInstance(inst Instance) error {
|
func (s *Store) UpdateInstance(inst Instance) error {
|
||||||
inst.UpdatedAt = Now()
|
inst.UpdatedAt = Now()
|
||||||
result, err := s.db.Exec(
|
result, err := s.db.Exec(
|
||||||
`UPDATE instances SET stage_id=?, project_id=?, container_id=?, image_tag=?, subdomain=?, npm_proxy_id=?, status=?, port=?, last_alive_at=?, updated_at=?
|
`UPDATE instances SET stage_id=?, project_id=?, container_id=?, image_tag=?, subdomain=?, npm_proxy_id=?, proxy_route_id=?, status=?, port=?, last_alive_at=?, updated_at=?
|
||||||
WHERE id=?`,
|
WHERE id=?`,
|
||||||
inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
||||||
inst.Subdomain, inst.NpmProxyID, inst.Status, inst.Port,
|
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
||||||
inst.LastAliveAt, inst.UpdatedAt, inst.ID,
|
inst.LastAliveAt, inst.UpdatedAt, inst.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type Settings struct {
|
|||||||
DNSProvider string `json:"dns_provider"`
|
DNSProvider string `json:"dns_provider"`
|
||||||
CloudflareAPIToken string `json:"cloudflare_api_token"`
|
CloudflareAPIToken string `json:"cloudflare_api_token"`
|
||||||
CloudflareZoneID string `json:"cloudflare_zone_id"`
|
CloudflareZoneID string `json:"cloudflare_zone_id"`
|
||||||
|
ProxyProvider string `json:"proxy_provider"`
|
||||||
BackupEnabled bool `json:"backup_enabled"`
|
BackupEnabled bool `json:"backup_enabled"`
|
||||||
BackupIntervalHours int `json:"backup_interval_hours"`
|
BackupIntervalHours int `json:"backup_interval_hours"`
|
||||||
BackupRetentionCount int `json:"backup_retention_count"`
|
BackupRetentionCount int `json:"backup_retention_count"`
|
||||||
@@ -97,9 +98,10 @@ type Instance struct {
|
|||||||
ContainerID string `json:"container_id"`
|
ContainerID string `json:"container_id"`
|
||||||
ImageTag string `json:"image_tag"`
|
ImageTag string `json:"image_tag"`
|
||||||
Subdomain string `json:"subdomain"`
|
Subdomain string `json:"subdomain"`
|
||||||
NpmProxyID int `json:"npm_proxy_id"`
|
NpmProxyID int `json:"npm_proxy_id"`
|
||||||
Status string `json:"status"` // running, stopped, failed, removing
|
ProxyRouteID string `json:"proxy_route_id"`
|
||||||
Port int `json:"port"`
|
Status string `json:"status"` // running, stopped, failed, removing
|
||||||
|
Port int `json:"port"`
|
||||||
LastAliveAt string `json:"last_alive_at"`
|
LastAliveAt string `json:"last_alive_at"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func (s *Store) GetSettings() (Settings, error) {
|
|||||||
base_volume_path, ssl_certificate_id, stale_threshold_days,
|
base_volume_path, ssl_certificate_id, stale_threshold_days,
|
||||||
allowed_volume_paths, wildcard_dns, dns_provider,
|
allowed_volume_paths, wildcard_dns, dns_provider,
|
||||||
cloudflare_api_token, cloudflare_zone_id,
|
cloudflare_api_token, cloudflare_zone_id,
|
||||||
|
proxy_provider,
|
||||||
backup_enabled, backup_interval_hours, backup_retention_count,
|
backup_enabled, backup_interval_hours, backup_retention_count,
|
||||||
updated_at
|
updated_at
|
||||||
FROM settings WHERE id = 1`,
|
FROM settings WHERE id = 1`,
|
||||||
@@ -22,6 +23,7 @@ func (s *Store) GetSettings() (Settings, error) {
|
|||||||
&st.BaseVolumePath, &st.SSLCertificateID, &st.StaleThresholdDays,
|
&st.BaseVolumePath, &st.SSLCertificateID, &st.StaleThresholdDays,
|
||||||
&st.AllowedVolumePaths, &wildcardDNS, &st.DNSProvider,
|
&st.AllowedVolumePaths, &wildcardDNS, &st.DNSProvider,
|
||||||
&st.CloudflareAPIToken, &st.CloudflareZoneID,
|
&st.CloudflareAPIToken, &st.CloudflareZoneID,
|
||||||
|
&st.ProxyProvider,
|
||||||
&backupEnabled, &st.BackupIntervalHours, &st.BackupRetentionCount,
|
&backupEnabled, &st.BackupIntervalHours, &st.BackupRetentionCount,
|
||||||
&st.UpdatedAt)
|
&st.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -50,6 +52,7 @@ func (s *Store) UpdateSettings(st Settings) error {
|
|||||||
base_volume_path=?, ssl_certificate_id=?, stale_threshold_days=?,
|
base_volume_path=?, ssl_certificate_id=?, stale_threshold_days=?,
|
||||||
allowed_volume_paths=?, wildcard_dns=?, dns_provider=?,
|
allowed_volume_paths=?, wildcard_dns=?, dns_provider=?,
|
||||||
cloudflare_api_token=?, cloudflare_zone_id=?,
|
cloudflare_api_token=?, cloudflare_zone_id=?,
|
||||||
|
proxy_provider=?,
|
||||||
backup_enabled=?, backup_interval_hours=?, backup_retention_count=?,
|
backup_enabled=?, backup_interval_hours=?, backup_retention_count=?,
|
||||||
updated_at=?
|
updated_at=?
|
||||||
WHERE id = 1`,
|
WHERE id = 1`,
|
||||||
@@ -58,6 +61,7 @@ func (s *Store) UpdateSettings(st Settings) error {
|
|||||||
st.BaseVolumePath, st.SSLCertificateID, st.StaleThresholdDays,
|
st.BaseVolumePath, st.SSLCertificateID, st.StaleThresholdDays,
|
||||||
st.AllowedVolumePaths, wildcardDNS, st.DNSProvider,
|
st.AllowedVolumePaths, wildcardDNS, st.DNSProvider,
|
||||||
st.CloudflareAPIToken, st.CloudflareZoneID,
|
st.CloudflareAPIToken, st.CloudflareZoneID,
|
||||||
|
st.ProxyProvider,
|
||||||
backupEnabled, st.BackupIntervalHours, st.BackupRetentionCount,
|
backupEnabled, st.BackupIntervalHours, st.BackupRetentionCount,
|
||||||
st.UpdatedAt,
|
st.UpdatedAt,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,6 +100,10 @@ func (s *Store) runMigrations() error {
|
|||||||
`ALTER TABLE settings ADD COLUMN backup_interval_hours INTEGER NOT NULL DEFAULT 24`,
|
`ALTER TABLE settings ADD COLUMN backup_interval_hours INTEGER NOT NULL DEFAULT 24`,
|
||||||
`ALTER TABLE settings ADD COLUMN backup_retention_count INTEGER NOT NULL DEFAULT 10`,
|
`ALTER TABLE settings ADD COLUMN backup_retention_count INTEGER NOT NULL DEFAULT 10`,
|
||||||
`ALTER TABLE stages ADD COLUMN notification_url TEXT NOT NULL DEFAULT ''`,
|
`ALTER TABLE stages ADD COLUMN notification_url TEXT NOT NULL DEFAULT ''`,
|
||||||
|
// Add proxy_route_id to instances for provider-agnostic route tracking (2026-04-04).
|
||||||
|
`ALTER TABLE instances ADD COLUMN proxy_route_id TEXT NOT NULL DEFAULT ''`,
|
||||||
|
// Add proxy_provider to settings (2026-04-04). Default to npm for backward compat.
|
||||||
|
`ALTER TABLE settings ADD COLUMN proxy_provider TEXT NOT NULL DEFAULT 'npm'`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range migrations {
|
for _, m := range migrations {
|
||||||
|
|||||||
Reference in New Issue
Block a user