af1dd14933
Second-pass logging cleanup: consistent [LEVEL] [module] format across all 41 files. Remove stale prefixes ([CF], [SYNC], [SCHED], [API], [STORAGE], [HEALTH], [ROLLBACK]). Remove 5 duplicate log lines. Gate ungated DEBUG lines. Fix wrong log levels (restore start WARN→INFO). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
315 lines
10 KiB
Go
315 lines
10 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 {
|
|
m.logger.Printf("[ERROR] [integrations] Integration %s apply failed: %v", key, err)
|
|
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] [integrations] Integration %s enabled", key)
|
|
} else {
|
|
start := time.Now()
|
|
if err := handler.Revoke(ac); err != nil {
|
|
m.logger.Printf("[WARN] [integrations] 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] [integrations] 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 {
|
|
m.logger.Printf("[WARN] [integrations] Failed to build context for %s:%s: provider stack not found", provider, target)
|
|
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] [integrations] 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] [integrations] 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] [integrations] 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 {
|
|
m.logger.Printf("[WARN] [integrations] Failed to load env for %s: app config is nil", s.Name)
|
|
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
|
|
}
|