feat: app-to-app integration framework + OnlyOffice handlers
Generic integration system for connecting deployed apps via toggle UI. First handlers: OnlyOffice→FileBrowser (config.yaml patch) and OnlyOffice→Nextcloud (occ CLI). Lifecycle hooks auto-suspend on stop and re-apply on start. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package integrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
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 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.
|
||||
func (m *Manager) OnStackStart(_ 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
|
||||
}
|
||||
|
||||
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 to re-apply
|
||||
provStack, pOk := m.stacks.GetStack(provider)
|
||||
if !pOk || !provStack.Deployed || provStack.State != stacks.StateRunning {
|
||||
continue
|
||||
}
|
||||
if target != "filebrowser" {
|
||||
tgtStack, tOk := m.stacks.GetStack(target)
|
||||
if !tOk || !tgtStack.Deployed || tgtStack.State != stacks.StateRunning {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
handler, hOk := m.handlers[key]
|
||||
if !hOk {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
for key, state := range all {
|
||||
provider, target, ok := ParseIntegrationKey(key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if state.Enabled {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user