65c0da4a2b
SyncFileBrowserMounts regenerates config.yaml from scratch, overwriting any integration config. The old approach used an async OnStackStart hook after container restart, which failed due to timing issues (stack state not yet refreshed). New approach: ReapplyConfigForTarget() writes integration config synchronously after config generation but before container restart, with a no-op RestartStack since the caller handles restart. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
246 lines
7.2 KiB
Go
246 lines
7.2 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
|
|
}
|
|
|
|
// 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{})
|
|
return m
|
|
}
|
|
|
|
// 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)
|
|
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 !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 !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)
|
|
}
|
|
}
|
|
|
|
if err := handler.Apply(ac); err != nil {
|
|
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)
|
|
}
|
|
state.Enabled = true
|
|
state.Status = "active"
|
|
state.EnabledAt = time.Now().UTC().Format(time.RFC3339)
|
|
m.logger.Printf("[INFO] Integration %s enabled", key)
|
|
} else {
|
|
if err := handler.Revoke(ac); err != nil {
|
|
m.logger.Printf("[WARN] Integration revoke failed for %s: %v", key, err)
|
|
state.LastError = err.Error()
|
|
}
|
|
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
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
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 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)
|
|
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 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)
|
|
}
|
|
return cfg.Env
|
|
}
|