mirror of
https://github.com/mudler/LocalAI
synced 2026-04-21 13:27:21 +00:00
fix(gallery): allow uninstalling orphaned meta backends + force reinstall (#9434)
Two interrelated bugs that combined to make a meta backend impossible to uninstall once its concrete had been removed from disk (partial install, earlier crash, manual cleanup). 1. DeleteBackendFromSystem returned "meta backend %q not found" and bailed out early when the concrete directory didn't exist, preventing the orphaned meta dir from ever being removed. Treat a missing concrete as idempotent success — log a warning and continue to remove the orphan meta. 2. InstallBackendFromGallery's "already installed, skip" short-circuit only checked that the name was known (`backends.Exists(name)`); an orphaned meta whose RunFile points at a missing concrete still satisfies that check, so every reinstall returned nil without doing anything. Afterwards the worker's findBackend returned empty and we kept looping with "backend %q not found after install attempt". Require the entry to be actually runnable (run.sh stat-able, not a directory) before skipping. New helper isBackendRunnable centralises the runnability test so both the install guard and future callers stay in sync. Tests cover the orphaned-meta delete path and the non-runnable short-circuit case.
This commit is contained in:
parent
28091d626e
commit
372eb08dcf
2 changed files with 91 additions and 6 deletions
|
|
@ -110,7 +110,13 @@ func InstallBackendFromGallery(ctx context.Context, galleries []config.Gallery,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if backends.Exists(name) {
|
||||
// Only short-circuit if the install is *actually usable*. An orphaned
|
||||
// meta entry whose concrete was removed still shows up in
|
||||
// ListSystemBackends with a RunFile pointing at a path that no longer
|
||||
// exists; returning early there leaves the caller with a broken
|
||||
// alias and the worker fails with "backend not found after install
|
||||
// attempt" on every retry. Re-install in that case.
|
||||
if existing, ok := backends.Get(name); ok && isBackendRunnable(existing) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -375,17 +381,44 @@ func DeleteBackendFromSystem(systemState *system.SystemState, name string) error
|
|||
}
|
||||
|
||||
if metadata != nil && metadata.MetaBackendFor != "" {
|
||||
metaBackendDirectory := filepath.Join(systemState.Backend.BackendsPath, metadata.MetaBackendFor)
|
||||
xlog.Debug("Deleting meta backend", "backendDirectory", metaBackendDirectory)
|
||||
if _, err := os.Stat(metaBackendDirectory); os.IsNotExist(err) {
|
||||
return fmt.Errorf("meta backend %q not found", metadata.MetaBackendFor)
|
||||
concreteDirectory := filepath.Join(systemState.Backend.BackendsPath, metadata.MetaBackendFor)
|
||||
xlog.Debug("Deleting concrete backend referenced by meta", "concreteDirectory", concreteDirectory)
|
||||
// If the concrete the meta points to is already gone (earlier delete,
|
||||
// partial install, or manual cleanup), keep going and remove the
|
||||
// orphaned meta dir. Previously we returned an error here, which made
|
||||
// the orphaned meta impossible to uninstall from the UI — the delete
|
||||
// kept failing and every subsequent install short-circuited because
|
||||
// the stale meta metadata made ListSystemBackends.Exists(name) true.
|
||||
if _, statErr := os.Stat(concreteDirectory); statErr == nil {
|
||||
os.RemoveAll(concreteDirectory)
|
||||
} else if os.IsNotExist(statErr) {
|
||||
xlog.Warn("Concrete backend referenced by meta not found — removing orphaned meta only",
|
||||
"meta", name, "concrete", metadata.MetaBackendFor)
|
||||
} else {
|
||||
return statErr
|
||||
}
|
||||
os.RemoveAll(metaBackendDirectory)
|
||||
}
|
||||
|
||||
return os.RemoveAll(backendDirectory)
|
||||
}
|
||||
|
||||
// isBackendRunnable reports whether the given backend entry can actually be
|
||||
// invoked. A meta backend is runnable only if its concrete's run.sh still
|
||||
// exists on disk; concrete backends are considered runnable as long as their
|
||||
// RunFile is set (ListSystemBackends only emits them when the runfile is
|
||||
// present). Used to guard the "already installed" short-circuit so an
|
||||
// orphaned meta pointing at a missing concrete triggers a real reinstall
|
||||
// rather than being silently skipped.
|
||||
func isBackendRunnable(b SystemBackend) bool {
|
||||
if b.RunFile == "" {
|
||||
return false
|
||||
}
|
||||
if fi, err := os.Stat(b.RunFile); err != nil || fi.IsDir() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type SystemBackend struct {
|
||||
Name string
|
||||
RunFile string
|
||||
|
|
|
|||
|
|
@ -952,6 +952,58 @@ var _ = Describe("Gallery Backends", func() {
|
|||
err = DeleteBackendFromSystem(systemState, "non-existent")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("removes an orphaned meta backend whose concrete is missing", func() {
|
||||
// Real scenario from the dev cluster: the concrete got wiped
|
||||
// (partial install, manual cleanup, previous crash) but the meta
|
||||
// directory + metadata.json still points at it. The old code
|
||||
// errored with "meta backend X not found" and left the orphan in
|
||||
// place, making the backend impossible to uninstall.
|
||||
metaName := "meta-backend"
|
||||
concreteName := "concrete-backend-that-vanished"
|
||||
metaPath := filepath.Join(tempDir, metaName)
|
||||
Expect(os.MkdirAll(metaPath, 0750)).To(Succeed())
|
||||
|
||||
meta := BackendMetadata{Name: metaName, MetaBackendFor: concreteName}
|
||||
data, err := json.MarshalIndent(meta, "", " ")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(os.WriteFile(filepath.Join(metaPath, "metadata.json"), data, 0644)).To(Succeed())
|
||||
|
||||
// Concrete directory intentionally absent.
|
||||
systemState, err := system.GetSystemState(system.WithBackendPath(tempDir))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(DeleteBackendFromSystem(systemState, metaName)).To(Succeed())
|
||||
Expect(metaPath).NotTo(BeADirectory())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("InstallBackendFromGallery — orphaned meta reinstall", func() {
|
||||
It("re-runs install when the meta's concrete is missing", func() {
|
||||
// Seed state: meta dir exists with metadata pointing at a
|
||||
// concrete that was removed from disk. ListSystemBackends still
|
||||
// surfaces the meta via its metadata.Name → the old short-circuit
|
||||
// at `if backends.Exists(name) { return nil }` returned silently,
|
||||
// leaving the worker's findBackend() with a dead alias forever.
|
||||
// The fix: require the backend to be runnable before we skip.
|
||||
metaName := "meta-orphan"
|
||||
concreteName := "concrete-gone"
|
||||
metaPath := filepath.Join(tempDir, metaName)
|
||||
Expect(os.MkdirAll(metaPath, 0750)).To(Succeed())
|
||||
meta := BackendMetadata{Name: metaName, MetaBackendFor: concreteName}
|
||||
data, err := json.MarshalIndent(meta, "", " ")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(os.WriteFile(filepath.Join(metaPath, "metadata.json"), data, 0644)).To(Succeed())
|
||||
|
||||
systemState, err := system.GetSystemState(system.WithBackendPath(tempDir))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
listed, err := ListSystemBackends(systemState)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
b, ok := listed.Get(metaName)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(isBackendRunnable(b)).To(BeFalse()) // concrete run.sh absent
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ListSystemBackends", func() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue