// Package deployer dispatches plugin-native Source deploys. The legacy // project-pipeline lived here until the hard cutover; what remains is a // thin holder for the Deployer's shared dependencies that `dispatch.go` // hands to every Source via PluginDeps(). package deployer import ( "fmt" "log/slog" "sync" "sync/atomic" "github.com/alexei/tinyforge/internal/dns" "github.com/alexei/tinyforge/internal/docker" "github.com/alexei/tinyforge/internal/events" "github.com/alexei/tinyforge/internal/health" "github.com/alexei/tinyforge/internal/notify" "github.com/alexei/tinyforge/internal/proxy" "github.com/alexei/tinyforge/internal/store" ) // Deployer owns the dependency bundle each Source plugin needs at deploy // time. The plugin pipeline reaches in via PluginDeps(); see dispatch.go // for the dispatch surface itself. type Deployer struct { docker *docker.Client proxy proxy.Provider store *store.Store health *health.Checker notifier *notify.Notifier eventBus EventPublisher backuper PreDeployBackuper // optional; nil disables pre-deploy backups encKey [32]byte dnsMu sync.RWMutex dns dns.Provider // nil when wildcard DNS is active // Graceful shutdown: tracks in-progress deploys. activeWg sync.WaitGroup shuttingDown atomic.Bool } // EventPublisher is the interface for publishing events to the event bus. type EventPublisher interface { Publish(evt events.Event) } // PreDeployBackuper takes a "pre-deploy" Tinyforge DB snapshot before any // deploy starts when the corresponding setting is enabled. Kept as a small // interface so the deployer does not import internal/backup. type PreDeployBackuper interface { CreateBackup(backupType string) (store.Backup, error) } // New creates a new Deployer with all required dependencies. func New( dockerClient *docker.Client, proxyProvider proxy.Provider, st *store.Store, checker *health.Checker, notifier *notify.Notifier, eventBus EventPublisher, encKey [32]byte, ) *Deployer { return &Deployer{ docker: dockerClient, proxy: proxyProvider, store: st, health: checker, notifier: notifier, eventBus: eventBus, encKey: encKey, } } // SetProxyProvider updates the proxy provider at runtime (e.g., when settings change). func (d *Deployer) SetProxyProvider(provider proxy.Provider) { d.proxy = provider } // SetPreDeployBackuper wires the backup engine in after construction so the // deployer can take a Tinyforge DB snapshot when the // auto_backup_before_deploy setting is enabled. Pass nil to disable. func (d *Deployer) SetPreDeployBackuper(b PreDeployBackuper) { d.backuper = b } // MaybeBackupBeforeDeploy creates a "pre-deploy" Tinyforge DB snapshot when // the setting is enabled. Failures are logged but do not abort the deploy: // missing a backup is preferable to refusing to ship a fix. Exposed so // Source plugins can opt into the same behaviour. func (d *Deployer) MaybeBackupBeforeDeploy(deployID string, settings store.Settings) { if !settings.AutoBackupBeforeDeploy || d.backuper == nil { return } backup, err := d.backuper.CreateBackup("pre-deploy") if err != nil { slog.Warn("pre-deploy backup failed", "deploy_id", deployID, "error", err) return } slog.Info("pre-deploy backup created", "deploy_id", deployID, "backup_id", backup.ID, "filename", backup.Filename) } // SetDNSProvider sets the DNS provider for managing DNS records during deployments. // Pass nil to disable DNS management (wildcard DNS mode). func (d *Deployer) SetDNSProvider(provider dns.Provider) { d.dnsMu.Lock() defer d.dnsMu.Unlock() d.dns = provider } // Drain waits for all in-progress deploys to complete. Call this during graceful shutdown. func (d *Deployer) Drain() { if !d.shuttingDown.CompareAndSwap(false, true) { // Already draining. } slog.Info("deployer: draining in-progress deploys") d.activeWg.Wait() slog.Info("deployer: all deploys drained") } // ShuttingDown reports whether Drain() has been called. func (d *Deployer) ShuttingDown() bool { return d.shuttingDown.Load() } // rejectIfDraining is exposed in case any plugin wants the same hard-stop // behaviour the legacy pipeline used. func (d *Deployer) rejectIfDraining() error { if d.shuttingDown.Load() { return fmt.Errorf("deployer is shutting down, rejecting new deploy") } return nil }