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
161 lines
5.4 KiB
Go
161 lines
5.4 KiB
Go
// SPDX-License-Identifier: MIT
|
|
package addons
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"sort"
|
|
|
|
"github.com/mudler/LocalAI/core/services/addons/catalog"
|
|
"github.com/mudler/LocalAI/core/services/addons/packages"
|
|
addonruntime "github.com/mudler/LocalAI/core/services/addons/runtime"
|
|
"github.com/mudler/LocalAI/pkg/addons/zipformat"
|
|
)
|
|
|
|
// OfficialReceipt reports whether a package came from the official add-on
|
|
// library that LocalAI ships and carries the official signature. The source ID
|
|
// alone is not enough, since an administrator names libraries freely.
|
|
func OfficialReceipt(r packages.PackageReceipt) bool {
|
|
return r.SourceID == catalog.OfficialLibraryID && r.Verification == "sha256+sigstore" && catalog.IsOfficialAddonURL(r.SourceURL)
|
|
}
|
|
|
|
// HostAccessInstalled is an installed add-on as the host access page needs it.
|
|
type HostAccessInstalled struct {
|
|
AddonID, Title, Version, State, Identity, SourceID string
|
|
Generation uint64
|
|
// OperationID is the lifecycle operation that completed the installed
|
|
// generation.
|
|
OperationID string
|
|
Official bool
|
|
Required, Optional []string
|
|
// NewOptional are optional operations that the installed version asks for,
|
|
// that the generation before did not, and that nothing grants yet.
|
|
NewOptional []string
|
|
// HeldVersion and HeldDelta describe an upgrade that waits for approval.
|
|
HeldVersion string
|
|
HeldDelta []string
|
|
}
|
|
|
|
type heldUpgrade struct {
|
|
version string
|
|
delta []string
|
|
}
|
|
|
|
func (m *Manager) holdUpgrade(id, version string, delta []string) {
|
|
m.autoMu.Lock()
|
|
defer m.autoMu.Unlock()
|
|
if m.heldUpgrades == nil {
|
|
m.heldUpgrades = map[string]heldUpgrade{}
|
|
}
|
|
m.heldUpgrades[id] = heldUpgrade{version: version, delta: slices.Clone(delta)}
|
|
}
|
|
|
|
func declared(r packages.PackageReceipt) (required, optional []string) {
|
|
if h := r.Manifest.Capabilities.HostOperations; h != nil {
|
|
return slices.Clone(h.Required), slices.Clone(h.Optional)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// HostAccessInstalled lists the installed add-ons with the host operations
|
|
// their verified manifests ask for. It never reads a grant: the caller asks
|
|
// the Authority.
|
|
func (m *Manager) HostAccessInstalled(ctx context.Context) ([]HostAccessInstalled, error) {
|
|
v, e := m.readStateLive(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
var out []HostAccessInstalled
|
|
for id, st := range v.addons {
|
|
r, ok := currentReceipt(st)
|
|
if !ok || !st.Installed {
|
|
continue
|
|
}
|
|
in := HostAccessInstalled{AddonID: id, Title: id, Version: r.Manifest.Version, Identity: st.Identity, SourceID: r.SourceID,
|
|
Generation: st.Generation, Official: OfficialReceipt(r), State: addonruntime.StateStopped}
|
|
if u := r.Manifest.Capabilities.UI; u != nil && u.Title != nil && u.Title.Default() != "" {
|
|
in.Title = u.Title.Default()
|
|
}
|
|
if st.DesiredEnabled && m.runtime != nil {
|
|
in.State = m.runtime.State(id, st.Generation)
|
|
}
|
|
if m.admit(r) != nil {
|
|
in.State = "failed"
|
|
}
|
|
in.Required, in.Optional = declared(r)
|
|
for _, g := range st.Generations {
|
|
if g.Number+1 == st.Generation {
|
|
pr, po := declared(g.Receipt)
|
|
for _, op := range in.Optional {
|
|
if !slices.Contains(pr, op) && !slices.Contains(po, op) {
|
|
if ok, _ := m.hostAuthority().Allows(id, op); !ok {
|
|
in.NewOptional = append(in.NewOptional, op)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for oid, op := range v.operations {
|
|
if op.Command.AddonID == id && op.Status.Phase == string(lifecycleCompleted) && op.Status.Generation == st.Generation &&
|
|
(op.Command.Action == "install" || op.Command.Action == "upgrade") && (in.OperationID == "" || oid < in.OperationID) {
|
|
in.OperationID = oid
|
|
}
|
|
}
|
|
m.autoMu.Lock()
|
|
if h, ok := m.heldUpgrades[id]; ok && h.version != in.Version {
|
|
in.HeldVersion, in.HeldDelta = h.version, slices.Clone(h.delta)
|
|
}
|
|
m.autoMu.Unlock()
|
|
out = append(out, in)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].AddonID < out[j].AddonID })
|
|
return out, nil
|
|
}
|
|
|
|
// HostAccessRelease is what the verified, staged package of a release asks for.
|
|
type HostAccessRelease struct {
|
|
AddonID, Title, Version, Digest string
|
|
Official, Installed bool
|
|
Required, Optional []string
|
|
}
|
|
|
|
// PreviewRelease resolves and stages a release, reads its verified manifest
|
|
// and discards the stage. It changes no state of the add-ons.
|
|
func (m *Manager) PreviewRelease(ctx context.Context, sourceID, addonID, version string) (HostAccessRelease, error) {
|
|
if m.installer == nil {
|
|
return HostAccessRelease{}, ErrForbidden
|
|
}
|
|
m.opMu.Lock()
|
|
defer m.opMu.Unlock()
|
|
m.mu.Lock()
|
|
if e := m.check(ctx); e != nil {
|
|
m.mu.Unlock()
|
|
return HostAccessRelease{}, e
|
|
}
|
|
installed := m.journal.Addons[addonID].Installed
|
|
m.mu.Unlock()
|
|
if sourceID == zipformat.SourceID {
|
|
// A local file: the version field names the staged file.
|
|
return m.previewFile(ctx, version)
|
|
}
|
|
r, e := m.resolveRelease(ctx, LifecycleCommand{SourceID: sourceID, AddonID: addonID, Version: version})
|
|
if e != nil {
|
|
return HostAccessRelease{}, e
|
|
}
|
|
staged, e := m.installer.Stage(ctx, r, nil)
|
|
if e != nil {
|
|
return HostAccessRelease{}, e
|
|
}
|
|
rc := staged.Receipt()
|
|
if e = m.installer.Discard(staged); e != nil {
|
|
return HostAccessRelease{}, e
|
|
}
|
|
out := HostAccessRelease{AddonID: rc.Manifest.ID, Title: rc.Manifest.ID, Version: rc.Manifest.Version, Digest: rc.Digest,
|
|
Official: OfficialReceipt(rc), Installed: installed}
|
|
if u := rc.Manifest.Capabilities.UI; u != nil && u.Title != nil && u.Title.Default() != "" {
|
|
out.Title = u.Title.Default()
|
|
}
|
|
out.Required, out.Optional = declared(rc)
|
|
return out, nil
|
|
}
|