mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 20:43:27 -05:00
An install or upgrade holds the owner's state lock through the backup, the commit and the start of the new generation. The apps list, the host access overview and the installed list took the same lock, so they waited for the whole operation and the frontends timed out after 65 s. The Add-ons page then had no list to show. Reads now work on a snapshot of the journal that every journal write publishes, and never wait for an operation. The apps list reports the running operation of each add-on (kind, phase, since), and the owner records the stage of an upgrade through the start of the candidate. A second upgrade of an add-on that is changing is refused with a conflict. A frontend waits eight seconds for a quick read of the owner. When the owner does not answer, the apps list returns the last list it got for the same person, marked stale for ten minutes. Without such a list the route answers a typed owner_busy or owner_unavailable 503, and the pins route no longer drops pins it could not check. Assisted-by: Claude:claude-sonnet-5-5
183 lines
6.4 KiB
Go
183 lines
6.4 KiB
Go
// SPDX-License-Identifier: MIT
|
|
package addons
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"github.com/mudler/LocalAI/pkg/addons/zipformat"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/core/services/addons/apps"
|
|
"github.com/mudler/LocalAI/core/services/addons/catalog"
|
|
"github.com/mudler/LocalAI/core/services/addons/updates"
|
|
"github.com/mudler/LocalAI/pkg/addons/contract"
|
|
)
|
|
|
|
// EnableUpdates attaches the owner's update check. It reads the journal and
|
|
// the libraries the manager already holds, bounded by the configuration. It is
|
|
// owner-only and set once, before commands are served; Host defaults to the
|
|
// installer's.
|
|
func (m *Manager) EnableUpdates(cfg updates.Config) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.installer != nil && cfg.Host == (contract.HostCompatibility{}) {
|
|
cfg.Host = m.installer.Host
|
|
}
|
|
// The check resolves the tag of a development build the way the install
|
|
// does, so both name the same digest.
|
|
if m.installer != nil && cfg.Lookup == nil {
|
|
cfg.Lookup = m.installer.Lookup
|
|
}
|
|
m.updates = updates.New(cfg, updates.FromCatalog(m.catalog), &updateReader{m: m})
|
|
}
|
|
|
|
// updateReader is the manager as the checker reads it. It takes the lock for
|
|
// one lookup and gives up when the request is gone, so a status read does not
|
|
// wait behind a long lifecycle command without bound.
|
|
type updateReader struct{ m *Manager }
|
|
|
|
func (r *updateReader) Installation(ctx context.Context, id string) (updates.Installation, bool, error) {
|
|
m := r.m
|
|
for !m.mu.TryLock() {
|
|
timer := time.NewTimer(10 * time.Millisecond)
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return updates.Installation{}, false, ctx.Err()
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
defer m.mu.Unlock()
|
|
if e := m.check(ctx); e != nil {
|
|
return updates.Installation{}, false, e
|
|
}
|
|
st, ok := m.journal.Addons[id]
|
|
if !ok || !st.Installed {
|
|
return updates.Installation{}, false, nil
|
|
}
|
|
receipt, ok := currentReceipt(st)
|
|
if !ok || receipt.SourceID == zipformat.SourceID {
|
|
// A package from a local file has no library to ask, so no check can
|
|
// answer for it. The page reads that as unknown, never as current.
|
|
return updates.Installation{}, false, nil
|
|
}
|
|
return updates.Installation{AddonID: id, Generation: st.Generation, Receipt: receipt}, true, nil
|
|
}
|
|
|
|
// checkPins validates an explicit upgrade at execution. The caller holds the
|
|
// lock and nothing has been recorded: every refusal here leaves the journal as
|
|
// it was. It returns the release the library resolves to now, which is the
|
|
// candidate the caller pinned.
|
|
func (m *Manager) checkPins(ctx context.Context, cmd LifecycleCommand, st Status) (catalog.Release, error) {
|
|
if m.updates == nil {
|
|
return catalog.Release{}, catalog.ErrUnavailable
|
|
}
|
|
receipt, ok := currentReceipt(st)
|
|
if !ok {
|
|
return catalog.Release{}, ErrConflict
|
|
}
|
|
if receipt.Digest != cmd.ExpectedDigest || (cmd.Generation != 0 && cmd.Generation != st.Generation) {
|
|
return catalog.Release{}, ErrStaleInstalled
|
|
}
|
|
res := m.updates.Evaluate(ctx, updates.Installation{AddonID: cmd.AddonID, Generation: st.Generation, Receipt: receipt})
|
|
switch res.Status.State {
|
|
case updates.IntegrityError:
|
|
return catalog.Release{}, ErrIntegrity
|
|
case updates.Incompatible:
|
|
return catalog.Release{}, ErrIncompatible
|
|
case updates.UpdateAvailable:
|
|
// A mutable entry (a development build) has no digest in the
|
|
// library: the candidate is the digest its tag named at this
|
|
// evaluation, and the install must resolve the tag to it again.
|
|
if res.Status.CandidateDigest != cmd.CandidateDigest || (!res.Release.Mutable && res.Release.Digest != cmd.CandidateDigest) {
|
|
return catalog.Release{}, ErrCandidateChanged
|
|
}
|
|
// Compatibility is judged on the release as resolved now.
|
|
if len(updates.Compatibility(m.installer.Host, receipt, res.Release)) > 0 {
|
|
return catalog.Release{}, ErrIncompatible
|
|
}
|
|
return res.Release, nil
|
|
case updates.UpToDate:
|
|
return catalog.Release{}, ErrCandidateChanged
|
|
}
|
|
return catalog.Release{}, ErrCheckFailed
|
|
}
|
|
|
|
// visibleTo reports whether the actor may read update status of an add-on: an
|
|
// administrator for any installed add-on, anyone else only for an enabled one.
|
|
// Hiding is the same denial for an add-on that does not exist.
|
|
func (m *Manager) visibleTo(ctx context.Context, actor contract.Actor, id string) (admin bool, e error) {
|
|
v, e := m.readStateLive(ctx)
|
|
if e != nil {
|
|
return false, e
|
|
}
|
|
admin = m.admin != nil && m.admin(ctx, actor) == nil
|
|
st, ok := v.addons[id]
|
|
if !addonID.MatchString(id) || !ok || !st.Installed || (!st.DesiredEnabled && !admin) {
|
|
return false, ErrForbidden
|
|
}
|
|
return admin, nil
|
|
}
|
|
|
|
// UpdateStatus is the update check result of one add-on as the actor may see
|
|
// it. It never uses the network: it serves the cached answer or evaluates the
|
|
// library data the owner already holds. A caller who is not an administrator
|
|
// gets the state, versions and times only.
|
|
func (m *Manager) UpdateStatus(ctx context.Context, actor contract.Actor, id string) (updates.Status, error) {
|
|
admin, e := m.visibleTo(ctx, actor, id)
|
|
if e != nil {
|
|
return updates.Status{}, e
|
|
}
|
|
if m.updates == nil {
|
|
return updates.Status{}, catalog.ErrUnavailable
|
|
}
|
|
st, e := m.updates.Check(ctx, id, false)
|
|
if e != nil {
|
|
return updates.Status{}, classifyUpdateError(e)
|
|
}
|
|
if !admin {
|
|
st = st.Redacted()
|
|
}
|
|
return st, nil
|
|
}
|
|
|
|
// CheckUpdates fetches the library of an installed add-on again and returns
|
|
// the new status. It is administrator-only, rate limited per add-on and
|
|
// library, and it applies nothing.
|
|
func (m *Manager) CheckUpdates(ctx context.Context, actor contract.Actor, id string) (updates.Status, error) {
|
|
if e := m.authorize(ctx, actor); e != nil {
|
|
return updates.Status{}, e
|
|
}
|
|
if !addonID.MatchString(id) {
|
|
return updates.Status{}, ErrForbidden
|
|
}
|
|
if m.updates == nil {
|
|
return updates.Status{}, catalog.ErrUnavailable
|
|
}
|
|
st, e := m.updates.Check(ctx, id, true)
|
|
if e != nil {
|
|
return updates.Status{}, classifyUpdateError(e)
|
|
}
|
|
return st, nil
|
|
}
|
|
|
|
func classifyUpdateError(e error) error {
|
|
if errors.Is(e, updates.ErrNotInstalled) {
|
|
return ErrForbidden
|
|
}
|
|
return e
|
|
}
|
|
|
|
// updateView is the management view of a status.
|
|
func updateView(st updates.Status) *apps.Update {
|
|
u := &apps.Update{Available: st.State == updates.UpdateAvailable, State: string(st.State), Version: st.CandidateVersion, Digest: st.CandidateDigest,
|
|
Stale: st.Stale, Reasons: st.Reasons, Error: st.Error}
|
|
if !st.CheckedAt.IsZero() {
|
|
u.CheckedAt = st.CheckedAt.UTC().Format(time.RFC3339)
|
|
}
|
|
if !st.LastSuccess.IsZero() {
|
|
u.LastSuccess = st.LastSuccess.UTC().Format(time.RFC3339)
|
|
}
|
|
return u
|
|
}
|