Files
deploy-felhom-compose/controller/internal/integrations/lifecycle.go
T
admin 95c821deb2 feat: comprehensive debug logging across all controller modules
Add detailed [DEBUG] logging to every controller module when
logging.level is set to "debug". Each module with stateful debug
uses SetDebug(bool) wired from main.go. Covers stacks, backup,
cloudflare, integrations, system, monitor, settings, scheduler,
web handlers, storage, metrics, API, selfupdate, and assets.

Also includes the app export/import (.fab bundles) feature from
v0.32.0 and its debug page integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:14:43 +01:00

192 lines
5.6 KiB
Go

package integrations
import (
"context"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// OnStackStop is called when a stack is stopped.
// Revokes active integrations where this stack is provider or target.
// Keeps enabled=true so OnStackStart can re-apply them later.
func (m *Manager) OnStackStop(_ context.Context, stackName string) {
m.mu.Lock()
defer m.mu.Unlock()
all := m.sett.GetIntegrationsForProvider(stackName)
targetAll := m.sett.GetIntegrationsForTarget(stackName)
for k, v := range targetAll {
all[k] = v
}
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackStop: stack=%s integrationsFound=%d", stackName, len(all))
}
for key, state := range all {
if !state.Enabled || state.Status == "disabled" {
continue
}
provider, target, ok := ParseIntegrationKey(key)
if !ok {
continue
}
handler, hOk := m.handlers[key]
if !hOk {
continue
}
ac, err := m.buildApplyContext(provider, target)
if err != nil {
m.logger.Printf("[WARN] Cannot build context for integration %s revoke: %v", key, err)
continue
}
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackStop: revoking %s", key)
}
if err := handler.Revoke(ac); err != nil {
m.logger.Printf("[WARN] Integration revoke on stop failed for %s: %v", key, err)
}
if provider == stackName {
state.Status = "provider_stopped"
} else {
state.Status = "target_unavailable"
}
_ = m.sett.SetIntegrationState(key, state)
m.logger.Printf("[INFO] Integration %s suspended (stack %s stopped)", key, stackName)
}
}
// OnStackStart is called when a stack starts.
// Re-applies integrations that were previously enabled but are not currently active.
// Waits briefly for the stack manager to refresh container state.
func (m *Manager) OnStackStart(_ context.Context, stackName string) {
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackStart: stack=%s, waiting 5s for state refresh", stackName)
}
// Brief delay so the stack manager's periodic status refresh
// picks up the new container state (runs every 30s).
time.Sleep(5 * time.Second)
m.mu.Lock()
defer m.mu.Unlock()
all := m.sett.GetIntegrationsForProvider(stackName)
targetAll := m.sett.GetIntegrationsForTarget(stackName)
for k, v := range targetAll {
all[k] = v
}
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackStart: stack=%s integrationsFound=%d", stackName, len(all))
}
for key, state := range all {
if !state.Enabled || state.Status == "active" {
continue
}
provider, target, ok := ParseIntegrationKey(key)
if !ok {
continue
}
// Both provider and target must be running (or starting) to re-apply.
// StateStarting = container running but healthcheck hasn't passed yet — still connectable.
provStack, pOk := m.stacks.GetStack(provider)
if !pOk || !provStack.Deployed || !isStackUp(provStack.State) {
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — provider %s not up (found=%v deployed=%v state=%v)", key, provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { if pOk { return provStack.State }; return "" }())
}
continue
}
if target != "filebrowser" {
tgtStack, tOk := m.stacks.GetStack(target)
if !tOk || !tgtStack.Deployed || !isStackUp(tgtStack.State) {
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — target %s not up (found=%v deployed=%v state=%v)", key, target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { if tOk { return tgtStack.State }; return "" }())
}
continue
}
}
handler, hOk := m.handlers[key]
if !hOk {
continue
}
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackStart: re-applying %s (currentStatus=%s)", key, state.Status)
}
ac, err := m.buildApplyContext(provider, target)
if err != nil {
m.logger.Printf("[WARN] Cannot re-apply integration %s on start: %v", key, err)
continue
}
if err := handler.Apply(ac); err != nil {
m.logger.Printf("[WARN] Integration re-apply on start failed for %s: %v", key, err)
state.Status = "error"
state.LastError = err.Error()
_ = m.sett.SetIntegrationState(key, state)
continue
}
state.Status = "active"
state.LastError = ""
_ = m.sett.SetIntegrationState(key, state)
m.logger.Printf("[INFO] Integration %s re-activated (stack %s started)", key, stackName)
}
}
// isStackUp returns true if the stack is running or starting (healthcheck pending).
func isStackUp(state stacks.ContainerState) bool {
return state == stacks.StateRunning || state == stacks.StateStarting
}
// OnStackRemove is called when a stack is removed.
// Permanently revokes and deletes integration state.
func (m *Manager) OnStackRemove(_ context.Context, stackName string) {
m.mu.Lock()
defer m.mu.Unlock()
all := m.sett.GetIntegrationsForProvider(stackName)
targetAll := m.sett.GetIntegrationsForTarget(stackName)
for k, v := range targetAll {
all[k] = v
}
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackRemove: stack=%s integrationsFound=%d", stackName, len(all))
}
for key, state := range all {
provider, target, ok := ParseIntegrationKey(key)
if !ok {
continue
}
if state.Enabled {
if m.isDebug() {
m.logger.Printf("[DEBUG] [integrations] OnStackRemove: revoking enabled integration %s", key)
}
handler, hOk := m.handlers[key]
if hOk {
ac, _ := m.buildApplyContext(provider, target)
if ac != nil {
_ = handler.Revoke(ac)
}
}
}
_ = m.sett.RemoveIntegrationState(key)
m.logger.Printf("[INFO] Integration %s removed (stack %s removed)", key, stackName)
}
}