cb9752b1a2
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
package plugins
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/kandev/kandev/internal/plugins/store"
|
|
)
|
|
|
|
// Registry is an in-memory, mutex-guarded index of installed plugins, keyed
|
|
// by plugin id. It is loaded from the filesystem store at startup (Load)
|
|
// and kept in sync as Service mutates installations, so read paths (List,
|
|
// Get) never hit disk.
|
|
//
|
|
// Get and List return copies of the stored *store.Record so callers cannot
|
|
// mutate registry state by holding onto a returned pointer; all writes go
|
|
// through Add / Remove / SetStatus / SetRuntimeState / SetAutoUpdate /
|
|
// SetRestartCount.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
byID map[string]*store.Record
|
|
}
|
|
|
|
// NewRegistry returns an empty Registry. Call Load to populate it from a
|
|
// store.Store at startup.
|
|
func NewRegistry() *Registry {
|
|
return &Registry{byID: make(map[string]*store.Record)}
|
|
}
|
|
|
|
// Load replaces the registry's contents with every record currently
|
|
// persisted in s. Intended to be called once at startup (see Provide).
|
|
func (r *Registry) Load(s store.Store) error {
|
|
records, err := s.List()
|
|
if err != nil {
|
|
return fmt.Errorf("load plugin registry: %w", err)
|
|
}
|
|
|
|
byID := make(map[string]*store.Record, len(records))
|
|
var migrationErrs []error
|
|
for _, rec := range records {
|
|
if rec.InstallationID == "" {
|
|
migrated := cloneRecord(rec)
|
|
migrated.InstallationID = uuid.NewString()
|
|
if err := s.Save(migrated); err != nil {
|
|
migrationErrs = append(migrationErrs, fmt.Errorf("migrate plugin installation id for %s: %w", rec.ID, err))
|
|
} else {
|
|
rec = migrated
|
|
}
|
|
}
|
|
byID[rec.ID] = cloneRecord(rec)
|
|
}
|
|
|
|
r.mu.Lock()
|
|
r.byID = byID
|
|
r.mu.Unlock()
|
|
return errors.Join(migrationErrs...)
|
|
}
|
|
|
|
// Get returns a copy of the record for id, and whether it was found.
|
|
func (r *Registry) Get(id string) (*store.Record, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
rec, ok := r.byID[id]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return cloneRecord(rec), true
|
|
}
|
|
|
|
// List returns a copy of every registered record, sorted by id for
|
|
// deterministic output.
|
|
func (r *Registry) List() []*store.Record {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]*store.Record, 0, len(r.byID))
|
|
for _, rec := range r.byID {
|
|
out = append(out, cloneRecord(rec))
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
|
return out
|
|
}
|
|
|
|
// Add inserts or replaces the record for record.ID.
|
|
func (r *Registry) Add(record *store.Record) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.byID[record.ID] = cloneRecord(record)
|
|
}
|
|
|
|
// Remove deletes the record for id, if present. A no-op if id is unknown.
|
|
func (r *Registry) Remove(id string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
delete(r.byID, id)
|
|
}
|
|
|
|
// SetStatus updates the in-memory status for id and returns a copy of the
|
|
// updated record. ok is false if id is not registered. Callers wanting FSM
|
|
// validation (Service.SetStatus) must check that before calling this — this
|
|
// method performs the raw mutation only.
|
|
func (r *Registry) SetStatus(id string, status Status) (*store.Record, bool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
rec, ok := r.byID[id]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
rec.Status = status
|
|
return cloneRecord(rec), true
|
|
}
|
|
|
|
// SetAutoUpdate updates the in-memory per-plugin auto-update override for id
|
|
// and returns a copy of the updated record. ok is false if id is not
|
|
// registered. Called by Service.SetPluginAutoUpdate, which persists the copy
|
|
// to the store; v is nil to clear the override (inherit the global default).
|
|
func (r *Registry) SetAutoUpdate(id string, v *bool) (*store.Record, bool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
rec, ok := r.byID[id]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
rec.AutoUpdate = v
|
|
return cloneRecord(rec), true
|
|
}
|
|
|
|
// SetRestartCount updates the in-memory restart count for id and returns a
|
|
// copy of the updated record. ok is false if id is not registered. Called
|
|
// by Service after a runtime.Manager-driven restart, to persist
|
|
// store.Record.RestartCount best-effort.
|
|
func (r *Registry) SetRestartCount(id string, count int) (*store.Record, bool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
rec, ok := r.byID[id]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
rec.RestartCount = count
|
|
return cloneRecord(rec), true
|
|
}
|
|
|
|
// SetRuntimeState updates the lifecycle status and persisted failure
|
|
// diagnostic for id and returns a copy of the resulting record. The caller is
|
|
// responsible for FSM validation and persistence; this method only performs
|
|
// the in-memory mutation under the registry lock.
|
|
func (r *Registry) SetRuntimeState(id string, status Status, lastError string, lastErrorAt *time.Time) (*store.Record, bool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
rec, ok := r.byID[id]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
rec.Status = status
|
|
rec.LastError = lastError
|
|
if lastErrorAt == nil {
|
|
rec.LastErrorAt = nil
|
|
} else {
|
|
at := *lastErrorAt
|
|
rec.LastErrorAt = &at
|
|
}
|
|
return cloneRecord(rec), true
|
|
}
|
|
|
|
func cloneRecord(rec *store.Record) *store.Record {
|
|
clone := *rec
|
|
clone.Manifest = rec.Clone()
|
|
if rec.AutoUpdate != nil {
|
|
autoUpdate := *rec.AutoUpdate
|
|
clone.AutoUpdate = &autoUpdate
|
|
}
|
|
if rec.LastErrorAt != nil {
|
|
at := *rec.LastErrorAt
|
|
clone.LastErrorAt = &at
|
|
}
|
|
return &clone
|
|
}
|
|
|
|
// activeRepositoryProviderOwner returns the active plugin that owns provider,
|
|
// excluding excludeID (used while validating an in-place upgrade).
|
|
func (r *Registry) activeRepositoryProviderOwner(provider, excludeID string) (string, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
for id, rec := range r.byID {
|
|
if id == excludeID || rec.Status != StatusActive {
|
|
continue
|
|
}
|
|
for _, declared := range rec.RepositoryProviders {
|
|
if strings.EqualFold(strings.TrimSpace(declared), strings.TrimSpace(provider)) {
|
|
return id, true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// activeReferenceSourceOwner returns the active plugin that owns source,
|
|
// excluding excludeID (used while validating an in-place upgrade).
|
|
func (r *Registry) activeReferenceSourceOwner(source, excludeID string) (string, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
for id, rec := range r.byID {
|
|
if id == excludeID || rec.Status != StatusActive {
|
|
continue
|
|
}
|
|
for _, declared := range rec.ReferenceSources {
|
|
if declared.Source == source {
|
|
return id, true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// activeReferenceProviderKindOwner returns the active plugin that owns the
|
|
// provider/kind pair, excluding excludeID (used while validating an in-place
|
|
// upgrade). Mention routing authorizes by this pair, so source uniqueness on
|
|
// its own cannot prevent ambiguous ownership.
|
|
func (r *Registry) activeReferenceProviderKindOwner(provider, kind, excludeID string) (string, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
for id, rec := range r.byID {
|
|
if id == excludeID || rec.Status != StatusActive {
|
|
continue
|
|
}
|
|
for _, declared := range rec.ReferenceSources {
|
|
if declared.Provider == provider && declared.Kind == kind {
|
|
return id, true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|