diff --git a/core/services/addons/catalog/official.go b/core/services/addons/catalog/official.go index d6b5496744..aabb5c9bc0 100644 --- a/core/services/addons/catalog/official.go +++ b/core/services/addons/catalog/official.go @@ -73,6 +73,10 @@ func isOfficialAddonURL(url string) bool { return ok && asset == OfficialReleaseAsset && releaseVersionRE.MatchString(version) } +// IsOfficialAddonURL reports whether url is the index of the official add-on +// library that LocalAI itself ships: the master index or the index of a release. +func IsOfficialAddonURL(url string) bool { return isOfficialAddonURL(url) } + const ( // OfficialBackendLibraryID is the source ID of the backend library. OfficialBackendLibraryID = "official-backends" diff --git a/core/services/addons/grants.go b/core/services/addons/grants.go index d38e4a3364..135e2e21ab 100644 --- a/core/services/addons/grants.go +++ b/core/services/addons/grants.go @@ -121,7 +121,8 @@ func (m *Manager) markCompleted(operationID string) { m.completed[operationID] = true } -// rebuildCompleted reads the completed operations from the journal just loaded. +// rebuildCompleted reads the completed operations and the identities of the +// installed add-ons from the journal just loaded. func (m *Manager) rebuildCompleted() { m.grantsMu.Lock() defer m.grantsMu.Unlock() @@ -131,6 +132,36 @@ func (m *Manager) rebuildCompleted() { m.completed[id] = true } } + m.identities = map[string]string{} + for id, st := range m.journal.Addons { + if st.Installed { + m.identities[id] = st.Identity + } + } +} + +// TrustedIdentity returns the trusted identity of the installed package of an +// add-on, or empty when it is not installed. A grant of the owner is bound to +// it. Like OperationCompleted it never takes the manager lock, so an Authority +// may call it from inside a check that the manager makes under that lock. +func (m *Manager) TrustedIdentity(addonID string) string { + m.grantsMu.Lock() + defer m.grantsMu.Unlock() + return m.identities[addonID] +} + +// noteIdentity records the identity an applied operation left behind. +func (m *Manager) noteIdentity(addonID string, st Status) { + m.grantsMu.Lock() + defer m.grantsMu.Unlock() + if m.identities == nil { + m.identities = map[string]string{} + } + if st.Installed { + m.identities[addonID] = st.Identity + return + } + delete(m.identities, addonID) } // pendingAuthority adds the operations an install or upgrade in flight is diff --git a/core/services/addons/manager.go b/core/services/addons/manager.go index d55f74041a..e6c2304de5 100644 --- a/core/services/addons/manager.go +++ b/core/services/addons/manager.go @@ -172,6 +172,9 @@ type Manager struct { pending map[string][]string // completed holds the operations that completed; see OperationCompleted. completed map[string]bool + // identities maps an installed add-on to its trusted identity; see + // TrustedIdentity. + identities map[string]string } // SetAuthority sets the Authority that admission and the apps list ask. The @@ -816,6 +819,9 @@ func (m *Manager) apply(op journalOperation) (OperationStatus, error) { m.journal.Operations[op.Command.OperationID] = op e := m.persist(string(lifecycleCompleted)) if e == nil { + // The identity first: an entry whose operation completed must not meet + // an add-on the Authority does not know yet. + m.noteIdentity(op.Command.AddonID, m.journal.Addons[op.Command.AddonID]) m.markCompleted(op.Command.OperationID) } if e == nil && uninstall { diff --git a/core/services/addons/manager_identity_test.go b/core/services/addons/manager_identity_test.go new file mode 100644 index 0000000000..f6ad926605 --- /dev/null +++ b/core/services/addons/manager_identity_test.go @@ -0,0 +1,33 @@ +//go:build linux + +// SPDX-License-Identifier: MIT +package addons + +import ( + "github.com/mudler/LocalAI/core/services/addons/catalog" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("trusted identity of an installed add-on", func() { + It("follows install and uninstall without the manager lock", func() { + r := newUpdateRig(false) + Expect(r.m.TrustedIdentity("sample")).To(BeEmpty()) + r.lib.transform = nil + rel := r.lib.publish("1.2.3-dev", "development", "a", catalog.Requirements{}) + r.lib.refresh() + _, e := r.m.Submit(ctx, actor, LifecycleCommand{OperationID: "i1", AddonID: "sample", Action: "install", SourceID: "test", Version: "development"}) + Expect(e).NotTo(HaveOccurred(), rel.Digest) + Expect(r.m.TrustedIdentity("sample")).To(Equal("disk-one:sample")) + + // The Authority calls it from inside admission, which holds the lock. + r.m.mu.Lock() + got := r.m.TrustedIdentity("sample") + r.m.mu.Unlock() + Expect(got).To(Equal("disk-one:sample")) + + _, e = r.m.Submit(ctx, actor, LifecycleCommand{OperationID: "x1", AddonID: "sample", Action: "uninstall"}) + Expect(e).NotTo(HaveOccurred()) + Expect(r.m.TrustedIdentity("sample")).To(BeEmpty()) + }) +})