mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-21 04:16:39 +00:00
131 lines
4.5 KiB
Go
131 lines
4.5 KiB
Go
package ldapsync
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/italypaleale/francis/actor"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
|
)
|
|
|
|
// The LdapSync singleton actor decides when the recurring LDAP synchronization runs.
|
|
|
|
// SyncActorType is the actor type for the LDAP sync actor
|
|
const SyncActorType = "LdapSync"
|
|
|
|
const (
|
|
// alarmSync is the name of the repeating alarm that runs the synchronization
|
|
alarmSync = "sync"
|
|
|
|
// syncInterval is how often the synchronization runs, as the ISO8601 duration the alarm repetition expects
|
|
// There's no jitter: the alarm is cluster-wide, so there are no replicas to spread apart
|
|
syncInterval = "PT1H"
|
|
|
|
// Delay the initial sync by 5s
|
|
initialSyncDelay = 5 * time.Second
|
|
|
|
// alarmTimeout bounds the alarm operations performed by the actor
|
|
alarmTimeout = 10 * time.Second
|
|
)
|
|
|
|
// syncActor is the cluster-wide singleton that triggers the recurring LDAP synchronization
|
|
type syncActor struct {
|
|
log *slog.Logger
|
|
service *Service
|
|
appConfig appconfig.AppConfigResolver
|
|
// scheduleDisabled removes the alarm instead of arming it, for environments that drive syncs explicitly
|
|
scheduleDisabled bool
|
|
client actor.Client[struct{}]
|
|
}
|
|
|
|
// NewSyncActor returns the factory that allocates the LDAP sync actor
|
|
func NewSyncActor(service *Service, appConfig appconfig.AppConfigResolver, scheduleDisabled bool) actor.Factory {
|
|
return func(actorID string, actorService *actor.Service) actor.Actor {
|
|
return &syncActor{
|
|
log: slog.With(
|
|
slog.String("scope", "actor"),
|
|
slog.String("actorType", SyncActorType),
|
|
),
|
|
service: service,
|
|
appConfig: appConfig,
|
|
scheduleDisabled: scheduleDisabled,
|
|
// The actor keeps no state of its own: the client is only used to manage the alarm
|
|
client: actor.NewActorClient[struct{}](SyncActorType, actorID, actorService),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bootstrap implements actor.ActorBootstrapper
|
|
// The host drives it on every startup, routed to the single owning host, so it must stay idempotent
|
|
func (a *syncActor) Bootstrap(parentCtx context.Context, _ actor.Envelope) error {
|
|
ctx, cancel := context.WithTimeout(parentCtx, alarmTimeout)
|
|
defer cancel()
|
|
|
|
// The schedule may have been enabled in a previous run, so make sure a leftover alarm doesn't keep firing
|
|
if a.scheduleDisabled {
|
|
err := a.client.DeleteAlarm(ctx, alarmSync)
|
|
if err != nil && !errors.Is(err, actor.ErrAlarmNotFound) {
|
|
return fmt.Errorf("error deleting the LDAP sync alarm: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Setting the alarm replaces whatever is registered, which both restores an alarm that was lost and picks up a change to the interval
|
|
// It's due right away (with a small delay) so the directory is synchronized as soon as the cluster starts, matching what the pre-actor scheduled job did
|
|
err := a.client.SetAlarm(ctx, alarmSync, actor.AlarmProperties{
|
|
DueTime: time.Now().Add(initialSyncDelay),
|
|
Interval: syncInterval,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("error setting the LDAP sync alarm: %w", err)
|
|
}
|
|
|
|
a.log.DebugContext(parentCtx, "Registered the recurring LDAP sync alarm", slog.String("interval", syncInterval))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Alarm implements actor.ActorAlarm
|
|
func (a *syncActor) Alarm(ctx context.Context, name string, _ actor.Envelope) error {
|
|
if name != alarmSync {
|
|
return fmt.Errorf("unsupported alarm '%s' for the %s actor", name, SyncActorType)
|
|
}
|
|
|
|
a.sync(ctx)
|
|
|
|
// A failed sync never surfaces as an error: the framework would retry the occurrence and then delete the alarm once the attempts run out, which would stop the synchronization altogether
|
|
// The next occurrence comes around on its own, exactly like the pre-actor scheduled job
|
|
return nil
|
|
}
|
|
|
|
// sync runs one synchronization, unless LDAP is disabled
|
|
// It logs failures rather than returning them, since the alarm has nowhere useful to send the error
|
|
func (a *syncActor) sync(ctx context.Context) {
|
|
dbConfig, err := a.appConfig.GetConfig(ctx)
|
|
if err != nil {
|
|
a.log.ErrorContext(ctx, "Failed to load the app configuration, skipping the LDAP sync", slog.Any("error", err))
|
|
return
|
|
}
|
|
|
|
if !dbConfig.LdapEnabled.IsTrue() {
|
|
a.log.DebugContext(ctx, "LDAP is disabled, skipping the sync")
|
|
return
|
|
}
|
|
|
|
a.log.InfoContext(ctx, "Starting the LDAP sync")
|
|
start := time.Now()
|
|
|
|
err = a.service.SyncAll(ctx, dbConfig)
|
|
if err != nil {
|
|
a.log.ErrorContext(ctx, "LDAP sync failed, will try again on the next run", slog.Duration("duration", time.Since(start)), slog.Any("error", err))
|
|
return
|
|
}
|
|
|
|
a.log.InfoContext(ctx, "LDAP sync completed", slog.Duration("duration", time.Since(start)))
|
|
}
|