95c821deb2
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>
312 lines
9.6 KiB
Go
312 lines
9.6 KiB
Go
package integrations
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
// StackProvider abstracts stacks.Manager to break circular imports.
|
|
type StackProvider interface {
|
|
GetStack(name string) (*stacks.Stack, bool)
|
|
GetStacks() []stacks.Stack
|
|
RestartStack(name string) error
|
|
}
|
|
|
|
// Manager coordinates integration apply/revoke and lifecycle hooks.
|
|
type Manager struct {
|
|
sett *settings.Settings
|
|
stacks StackProvider
|
|
domain string
|
|
stacksDir string
|
|
encKey []byte
|
|
logger *log.Logger
|
|
|
|
handlers map[string]Handler // key: "provider:target" -> Handler
|
|
mu sync.Mutex // serialize apply/revoke operations
|
|
debug bool
|
|
}
|
|
|
|
// NewManager creates an integration manager and registers built-in handlers.
|
|
func NewManager(sett *settings.Settings, sp StackProvider, domain, stacksDir string, encKey []byte, logger *log.Logger) *Manager {
|
|
m := &Manager{
|
|
sett: sett,
|
|
stacks: sp,
|
|
domain: domain,
|
|
stacksDir: stacksDir,
|
|
encKey: encKey,
|
|
logger: logger,
|
|
handlers: make(map[string]Handler),
|
|
}
|
|
// Register built-in handlers
|
|
m.RegisterHandler("onlyoffice:filebrowser", &OnlyOfficeFileBrowserHandler{})
|
|
m.RegisterHandler("onlyoffice:nextcloud", &OnlyOfficeNextcloudHandler{})
|
|
|
|
if m.isDebug() {
|
|
keys := make([]string, 0, len(m.handlers))
|
|
for k := range m.handlers {
|
|
keys = append(keys, k)
|
|
}
|
|
m.logger.Printf("[DEBUG] [integrations] NewManager: registered handlers: %v", keys)
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// SetDebug enables or disables debug logging.
|
|
func (m *Manager) SetDebug(debug bool) {
|
|
m.debug = debug
|
|
}
|
|
|
|
// isDebug returns whether debug logging is enabled.
|
|
func (m *Manager) isDebug() bool {
|
|
return m.debug
|
|
}
|
|
|
|
// RegisterHandler registers a handler for a provider:target integration key.
|
|
func (m *Manager) RegisterHandler(key string, h Handler) {
|
|
m.handlers[key] = h
|
|
}
|
|
|
|
// Toggle enables or disables an integration.
|
|
func (m *Manager) Toggle(ctx context.Context, provider, target string, enable bool) (settings.IntegrationState, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
key := IntegrationKey(provider, target)
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] Toggle: key=%s provider=%s target=%s enable=%v", key, provider, target, enable)
|
|
}
|
|
|
|
handler, ok := m.handlers[key]
|
|
if !ok {
|
|
return settings.IntegrationState{}, fmt.Errorf("nincs kezelő a(z) %s integrációhoz", key)
|
|
}
|
|
|
|
ac, err := m.buildApplyContext(provider, target)
|
|
if err != nil {
|
|
return settings.IntegrationState{}, err
|
|
}
|
|
|
|
var state settings.IntegrationState
|
|
if enable {
|
|
// Validate: provider must be deployed and running
|
|
provStack, pOk := m.stacks.GetStack(provider)
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] Toggle: provider %s found=%v deployed=%v state=%v", provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { if pOk { return provStack.State }; return "" }())
|
|
}
|
|
if !pOk || !provStack.Deployed {
|
|
return state, fmt.Errorf("a szolgáltató alkalmazás (%s) nincs telepítve", provider)
|
|
}
|
|
if provStack.State != stacks.StateRunning {
|
|
return state, fmt.Errorf("a szolgáltató alkalmazás (%s) nem fut", provider)
|
|
}
|
|
|
|
// Validate: target must be deployed and running (filebrowser is infra, always present)
|
|
if target != "filebrowser" {
|
|
tgtStack, tOk := m.stacks.GetStack(target)
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] Toggle: target %s found=%v deployed=%v state=%v", target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { if tOk { return tgtStack.State }; return "" }())
|
|
}
|
|
if !tOk || !tgtStack.Deployed {
|
|
return state, fmt.Errorf("a célalkalmazás (%s) nincs telepítve", target)
|
|
}
|
|
if tgtStack.State != stacks.StateRunning {
|
|
return state, fmt.Errorf("a célalkalmazás (%s) nem fut", target)
|
|
}
|
|
}
|
|
|
|
start := time.Now()
|
|
if err := handler.Apply(ac); err != nil {
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] Toggle: Apply failed for %s in %v: %v", key, time.Since(start), err)
|
|
}
|
|
state.Enabled = true
|
|
state.Status = "error"
|
|
state.LastError = err.Error()
|
|
state.EnabledAt = time.Now().UTC().Format(time.RFC3339)
|
|
_ = m.sett.SetIntegrationState(key, state)
|
|
return state, fmt.Errorf("integráció alkalmazása sikertelen: %w", err)
|
|
}
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] Toggle: Apply succeeded for %s in %v", key, time.Since(start))
|
|
}
|
|
state.Enabled = true
|
|
state.Status = "active"
|
|
state.EnabledAt = time.Now().UTC().Format(time.RFC3339)
|
|
m.logger.Printf("[INFO] Integration %s enabled", key)
|
|
} else {
|
|
start := time.Now()
|
|
if err := handler.Revoke(ac); err != nil {
|
|
m.logger.Printf("[WARN] Integration revoke failed for %s: %v", key, err)
|
|
state.LastError = err.Error()
|
|
}
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] Toggle: Revoke for %s completed in %v", key, time.Since(start))
|
|
}
|
|
state.Enabled = false
|
|
state.Status = "disabled"
|
|
m.logger.Printf("[INFO] Integration %s disabled", key)
|
|
}
|
|
|
|
if err := m.sett.SetIntegrationState(key, state); err != nil {
|
|
return state, fmt.Errorf("integráció állapotának mentése sikertelen: %w", err)
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
// ListForProvider returns StatusInfo for all integrations defined in a provider's metadata.
|
|
func (m *Manager) ListForProvider(providerSlug string) []StatusInfo {
|
|
provStack, ok := m.stacks.GetStack(providerSlug)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] ListForProvider: provider=%s integrationDefs=%d", providerSlug, len(provStack.Meta.Integrations))
|
|
}
|
|
|
|
var result []StatusInfo
|
|
for _, idef := range provStack.Meta.Integrations {
|
|
key := IntegrationKey(providerSlug, idef.Target)
|
|
state, _ := m.sett.GetIntegrationState(key)
|
|
|
|
targetDeployed := false
|
|
targetRunning := false
|
|
if tgt, tOk := m.stacks.GetStack(idef.Target); tOk {
|
|
// FileBrowser is infrastructure — if its stack exists, it's "deployed"
|
|
if idef.Target == "filebrowser" {
|
|
targetDeployed = true
|
|
} else {
|
|
targetDeployed = tgt.Deployed
|
|
}
|
|
targetRunning = tgt.State == stacks.StateRunning
|
|
}
|
|
|
|
result = append(result, StatusInfo{
|
|
Key: key,
|
|
Provider: providerSlug,
|
|
Target: idef.Target,
|
|
Label: idef.Label,
|
|
Description: idef.Description,
|
|
Enabled: state.Enabled,
|
|
Status: state.Status,
|
|
LastError: state.LastError,
|
|
TargetDeployed: targetDeployed,
|
|
TargetRunning: targetRunning,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// buildApplyContext gathers all data needed by a handler.
|
|
func (m *Manager) buildApplyContext(provider, target string) (*ApplyContext, error) {
|
|
provStack, ok := m.stacks.GetStack(provider)
|
|
if !ok {
|
|
return nil, fmt.Errorf("szolgáltató stack %q nem található", provider)
|
|
}
|
|
|
|
// Load decrypted env from provider's app.yaml
|
|
provEnv := m.loadDecryptedEnv(provStack)
|
|
|
|
if m.isDebug() {
|
|
envKeyCount := len(provEnv)
|
|
m.logger.Printf("[DEBUG] [integrations] buildApplyContext: provider=%s target=%s domain=%s envKeys=%d", provider, target, m.domain, envKeyCount)
|
|
}
|
|
|
|
provMeta := provStack.Meta
|
|
return &ApplyContext{
|
|
ProviderName: provider,
|
|
TargetName: target,
|
|
Domain: m.domain,
|
|
ProviderEnv: provEnv,
|
|
ProviderMeta: &provMeta,
|
|
StacksDir: m.stacksDir,
|
|
Logger: m.logger,
|
|
RestartStack: m.stacks.RestartStack,
|
|
}, nil
|
|
}
|
|
|
|
// ReapplyConfigForTarget applies all active integrations targeting the given stack,
|
|
// writing config changes only (no container restart). Use this when the caller
|
|
// handles container restart separately (e.g. SyncFileBrowserMounts).
|
|
func (m *Manager) ReapplyConfigForTarget(targetName string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
all := m.sett.GetIntegrationsForTarget(targetName)
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] ReapplyConfigForTarget: target=%s integrations=%d", targetName, 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
|
|
}
|
|
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] ReapplyConfigForTarget: reapplying %s (status=%s)", key, state.Status)
|
|
}
|
|
|
|
ac, err := m.buildApplyContext(provider, target)
|
|
if err != nil {
|
|
m.logger.Printf("[WARN] Cannot build context for integration %s reapply: %v", key, err)
|
|
continue
|
|
}
|
|
|
|
// Override RestartStack to no-op — caller handles restart
|
|
ac.RestartStack = func(string) error { return nil }
|
|
|
|
if err := handler.Apply(ac); err != nil {
|
|
m.logger.Printf("[WARN] Integration config reapply failed for %s: %v", key, err)
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] ReapplyConfigForTarget: %s failed: %v", key, err)
|
|
}
|
|
state.Status = "error"
|
|
state.LastError = err.Error()
|
|
_ = m.sett.SetIntegrationState(key, state)
|
|
continue
|
|
}
|
|
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] ReapplyConfigForTarget: %s succeeded", key)
|
|
}
|
|
state.Status = "active"
|
|
state.LastError = ""
|
|
_ = m.sett.SetIntegrationState(key, state)
|
|
m.logger.Printf("[INFO] Integration %s config reapplied for %s", key, targetName)
|
|
}
|
|
}
|
|
|
|
// loadDecryptedEnv reads app.yaml for a stack and decrypts sensitive values.
|
|
func (m *Manager) loadDecryptedEnv(s *stacks.Stack) map[string]string {
|
|
stackDir := filepath.Dir(s.ComposePath)
|
|
cfg := stacks.LoadAppConfig(stackDir)
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
if m.encKey != nil {
|
|
cfg.Env = crypto.DecryptMap(m.encKey, cfg.Env)
|
|
}
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [integrations] loadDecryptedEnv: stack=%s envKeys=%d", s.Name, len(cfg.Env))
|
|
}
|
|
return cfg.Env
|
|
}
|