Files
member-console/internal/workflows/fedwiki/sync.go
Christian Galo 675a4d93a3 Buffer template rendering and fix FedWiki sync
Introduce SafeTemplates.Render to execute templates into a buffer and
prevent partial HTML on errors. Replace direct ExecuteTemplate calls in
partial handlers and add a make lint-templates target to catch bypasses.
Update operator sites template/view model to use OwnerOrgName. Guard the
FedWiki sync by skipping inserts when DefaultWorkspaceID is empty and
scope deletes to the configured default workspace only.
2026-03-29 04:58:02 -05:00

206 lines
6.7 KiB
Go

package fedwiki
import (
"context"
"fmt"
"log/slog"
"time"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/fedwiki"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
)
// SyncFedWikiSitesWorkflowInput is the input for the SyncFedWikiSitesWorkflow.
type SyncFedWikiSitesWorkflowInput struct {
// DefaultWorkspaceID is the workspace ID to assign to sites that don't have a known owner.
DefaultWorkspaceID string
}
// SyncFedWikiSitesWorkflowOutput is the output for the SyncFedWikiSitesWorkflow.
type SyncFedWikiSitesWorkflowOutput struct {
Success bool
SitesFound int
SitesAdded int
SitesRemoved int
ErrorMessage string
SyncTimestamp time.Time
}
// SyncActivityOptions returns activity options for sync operations.
func SyncActivityOptions() workflow.ActivityOptions {
return workflow.ActivityOptions{
StartToCloseTimeout: 5 * time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: time.Minute,
MaximumAttempts: 5,
},
}
}
// SyncFedWikiSitesWorkflow synchronizes sites from the FedWiki farm to the local database.
// It fetches all sites from the FarmManager API and ensures the local database reflects
// the current state of the farm.
func SyncFedWikiSitesWorkflow(ctx workflow.Context, input SyncFedWikiSitesWorkflowInput) (*SyncFedWikiSitesWorkflowOutput, error) {
logger := workflow.GetLogger(ctx)
logger.Info("SyncFedWikiSitesWorkflow started",
"defaultWorkspaceID", input.DefaultWorkspaceID)
var activities *Activities
activityCtx := workflow.WithActivityOptions(ctx, SyncActivityOptions())
// Step 1: Fetch all sites from the FedWiki farm
var listResult *ListFedWikiSitesOutput
err := workflow.ExecuteActivity(activityCtx, activities.ListFedWikiSitesActivity, ListFedWikiSitesInput{}).Get(ctx, &listResult)
if err != nil {
logger.Error("Failed to list sites from FedWiki farm", "error", err)
return &SyncFedWikiSitesWorkflowOutput{
Success: false,
ErrorMessage: "Failed to fetch sites from FedWiki farm. Please try again later.",
SyncTimestamp: workflow.Now(ctx),
}, nil
}
logger.Info("Fetched sites from FedWiki farm", "count", len(listResult.Sites))
// Step 2: Sync the sites to the local database
var syncResult *SyncSitesToDBOutput
err = workflow.ExecuteActivity(activityCtx, activities.SyncSitesToDBActivity, SyncSitesToDBInput{
FarmSites: listResult.Sites,
DefaultWorkspaceID: input.DefaultWorkspaceID,
}).Get(ctx, &syncResult)
if err != nil {
logger.Error("Failed to sync sites to database", "error", err)
return &SyncFedWikiSitesWorkflowOutput{
Success: false,
SitesFound: len(listResult.Sites),
ErrorMessage: "Failed to sync sites to local database.",
SyncTimestamp: workflow.Now(ctx),
}, nil
}
logger.Info("SyncFedWikiSitesWorkflow completed successfully",
"sitesFound", len(listResult.Sites),
"sitesAdded", syncResult.SitesAdded,
"sitesRemoved", syncResult.SitesRemoved)
return &SyncFedWikiSitesWorkflowOutput{
Success: true,
SitesFound: len(listResult.Sites),
SitesAdded: syncResult.SitesAdded,
SitesRemoved: syncResult.SitesRemoved,
SyncTimestamp: workflow.Now(ctx),
}, nil
}
// SyncSitesToDBInput is the input for the SyncSitesToDB activity.
type SyncSitesToDBInput struct {
FarmSites []SiteInfo
DefaultWorkspaceID string
}
// SyncSitesToDBOutput is the output for the SyncSitesToDB activity.
type SyncSitesToDBOutput struct {
SitesAdded int
SitesUpdated int
SitesRemoved int
}
// SyncSitesToDBActivity synchronizes sites from the FedWiki farm to the local database.
// It adds new farm sites to the default workspace and removes stale sites from the default workspace.
// Sites created by users (belonging to non-default workspaces) are never modified.
func (a *Activities) SyncSitesToDBActivity(ctx context.Context, input SyncSitesToDBInput) (*SyncSitesToDBOutput, error) {
a.Logger.Info("syncing sites to database",
slog.Int("farmSiteCount", len(input.FarmSites)),
slog.String("defaultWorkspaceID", input.DefaultWorkspaceID))
// If no default workspace is configured, we cannot add or scope deletes — skip gracefully
if input.DefaultWorkspaceID == "" {
a.Logger.Warn("no default workspace ID configured, skipping site sync")
return &SyncSitesToDBOutput{}, nil
}
// Get all current sites from the local database
localSites, err := a.SiteQ.ListAllSites(ctx)
if err != nil {
a.Logger.Error("failed to get local sites", slog.Any("error", err))
return nil, fmt.Errorf("failed to get local sites: %w", err)
}
// Create a map of local sites by domain for quick lookup
localSiteMap := make(map[string]fwmod.Site, len(localSites))
for _, site := range localSites {
localSiteMap[site.Domain] = site
}
// Create a map of farm domains for quick lookup
farmDomainMap := make(map[string]SiteInfo, len(input.FarmSites))
for _, site := range input.FarmSites {
farmDomainMap[site.Name] = site
}
sitesAdded := 0
sitesUpdated := 0
sitesRemoved := 0
// Add sites from the farm that don't exist locally
for _, farmSite := range input.FarmSites {
if farmSite.Status != "active" {
a.Logger.Debug("skipping inactive site", slog.String("domain", farmSite.Name), slog.String("status", farmSite.Status))
continue
}
_, exists := localSiteMap[farmSite.Name]
if !exists {
_, err := a.SiteQ.UpsertSiteByDomain(ctx, fwmod.UpsertSiteByDomainParams{
WorkspaceID: input.DefaultWorkspaceID,
Domain: farmSite.Name,
IsCustomDomain: false,
})
if err != nil {
a.Logger.Error("failed to add site",
slog.String("domain", farmSite.Name),
slog.Any("error", err))
continue
}
sitesAdded++
a.Logger.Info("added site from farm",
slog.String("domain", farmSite.Name))
}
}
// Remove sites that belong to the default workspace and no longer exist on the farm.
// Sites belonging to other workspaces (user-created) are never touched.
for _, localSite := range localSites {
if localSite.WorkspaceID != input.DefaultWorkspaceID {
continue // user-created site, never delete
}
farmSite, exists := farmDomainMap[localSite.Domain]
if !exists || farmSite.Status != "active" {
err := a.SiteQ.DeleteSiteByDomain(ctx, localSite.Domain)
if err != nil {
a.Logger.Error("failed to delete site",
slog.String("domain", localSite.Domain),
slog.Any("error", err))
continue
}
sitesRemoved++
a.Logger.Info("removed stale site from default workspace", slog.String("domain", localSite.Domain))
}
}
a.Logger.Info("site sync completed",
slog.Int("sitesAdded", sitesAdded),
slog.Int("sitesUpdated", sitesUpdated),
slog.Int("sitesRemoved", sitesRemoved))
return &SyncSitesToDBOutput{
SitesAdded: sitesAdded,
SitesUpdated: sitesUpdated,
SitesRemoved: sitesRemoved,
}, nil
}